* [RFC PATCH v2 01/13] arch: add HAVE_REINSTALL_HW_BREAKPOINT
2026-07-17 12:50 [RFC PATCH v2 00/13] mm/kwatch: dynamic hardware watchpoints for hunting memory corruption Jinchao Wang
@ 2026-07-17 13:02 ` Jinchao Wang
2026-07-17 13:02 ` [RFC PATCH v2 02/13] x86/hw_breakpoint: Unify breakpoint install/uninstall Jinchao Wang
` (12 subsequent siblings)
13 siblings, 0 replies; 24+ messages in thread
From: Jinchao Wang @ 2026-07-17 13:02 UTC (permalink / raw)
To: Andrew Morton, Peter Zijlstra, Thomas Gleixner, Steven Rostedt,
Masami Hiramatsu
Cc: Ingo Molnar, Borislav Petkov, Dave Hansen, H . Peter Anvin, x86,
Arnaldo Carvalho de Melo, Namhyung Kim, Mark Rutland,
Mathieu Desnoyers, David Hildenbrand, Jonathan Corbet,
Matthew Wilcox, Alan Stern, Randy Dunlap, Alexander Potapenko,
Marco Elver, Mike Rapoport, linux-kernel, linux-mm,
linux-trace-kernel, linux-perf-users, linux-doc, Jinchao Wang
Some architectures can update the address, length or type of an
installed hardware breakpoint in place, without releasing and
re-reserving its slot. Add an opt-in Kconfig symbol so generic code
can offer such an operation on architectures that implement
arch_reinstall_hw_breakpoint().
This is a prerequisite for KWatch, which re-points preallocated
per-CPU breakpoints from atomic context, where the register/release
path (which may sleep and rebalances slot constraints) cannot be
used.
Signed-off-by: Jinchao Wang <wangjinchao600@gmail.com>
---
arch/Kconfig | 10 ++++++++++
1 file changed, 10 insertions(+)
diff --git a/arch/Kconfig b/arch/Kconfig
index fa7507ac8e13..41b3784e0ddd 100644
--- a/arch/Kconfig
+++ b/arch/Kconfig
@@ -457,6 +457,16 @@ config HAVE_MIXED_BREAKPOINTS_REGS
Select this option if your arch implements breakpoints under the
latter fashion.
+config HAVE_REINSTALL_HW_BREAKPOINT
+ bool
+ depends on HAVE_HW_BREAKPOINT
+ help
+ Depending on the arch implementation of hardware breakpoints,
+ some of them are able to update the breakpoint configuration
+ without release and reserve the hardware breakpoint register.
+ What configuration is able to update depends on hardware and
+ software implementation.
+
config HAVE_USER_RETURN_NOTIFIER
bool
--
2.53.0
^ permalink raw reply related [flat|nested] 24+ messages in thread* [RFC PATCH v2 02/13] x86/hw_breakpoint: Unify breakpoint install/uninstall
2026-07-17 12:50 [RFC PATCH v2 00/13] mm/kwatch: dynamic hardware watchpoints for hunting memory corruption Jinchao Wang
2026-07-17 13:02 ` [RFC PATCH v2 01/13] arch: add HAVE_REINSTALL_HW_BREAKPOINT Jinchao Wang
@ 2026-07-17 13:02 ` Jinchao Wang
2026-07-17 13:15 ` sashiko-bot
2026-07-17 13:03 ` [RFC PATCH v2 03/13] x86/hw_breakpoint: Add arch_reinstall_hw_breakpoint Jinchao Wang
` (11 subsequent siblings)
13 siblings, 1 reply; 24+ messages in thread
From: Jinchao Wang @ 2026-07-17 13:02 UTC (permalink / raw)
To: Andrew Morton, Peter Zijlstra, Thomas Gleixner, Steven Rostedt,
Masami Hiramatsu
Cc: Ingo Molnar, Borislav Petkov, Dave Hansen, H . Peter Anvin, x86,
Arnaldo Carvalho de Melo, Namhyung Kim, Mark Rutland,
Mathieu Desnoyers, David Hildenbrand, Jonathan Corbet,
Matthew Wilcox, Alan Stern, Randy Dunlap, Alexander Potapenko,
Marco Elver, Mike Rapoport, linux-kernel, linux-mm,
linux-trace-kernel, linux-perf-users, linux-doc, Jinchao Wang
Consolidate breakpoint management to reduce code duplication.
The diffstat was misleading, so the stripped code size is compared instead.
After refactoring, it is reduced from 11976 bytes to 11448 bytes on my
x86_64 system built with clang.
This also makes it easier to introduce arch_reinstall_hw_breakpoint().
In addition, including linux/types.h to fix a missing build dependency.
Signed-off-by: Jinchao Wang <wangjinchao600@gmail.com>
Reviewed-by: Masami Hiramatsu (Google) <mhiramat@kernel.org>
---
arch/x86/include/asm/hw_breakpoint.h | 6 ++
arch/x86/kernel/hw_breakpoint.c | 153 ++++++++++++++++-----------
2 files changed, 95 insertions(+), 64 deletions(-)
diff --git a/arch/x86/include/asm/hw_breakpoint.h b/arch/x86/include/asm/hw_breakpoint.h
index 0bc931cd0698..aa6adac6c3a2 100644
--- a/arch/x86/include/asm/hw_breakpoint.h
+++ b/arch/x86/include/asm/hw_breakpoint.h
@@ -5,6 +5,7 @@
#include <uapi/asm/hw_breakpoint.h>
#define __ARCH_HW_BREAKPOINT_H
+#include <linux/types.h>
/*
* The name should probably be something dealt in
@@ -18,6 +19,11 @@ struct arch_hw_breakpoint {
u8 type;
};
+enum bp_slot_action {
+ BP_SLOT_ACTION_INSTALL,
+ BP_SLOT_ACTION_UNINSTALL,
+};
+
#include <linux/kdebug.h>
#include <linux/percpu.h>
#include <linux/list.h>
diff --git a/arch/x86/kernel/hw_breakpoint.c b/arch/x86/kernel/hw_breakpoint.c
index f846c15f21ca..76886467708b 100644
--- a/arch/x86/kernel/hw_breakpoint.c
+++ b/arch/x86/kernel/hw_breakpoint.c
@@ -49,7 +49,6 @@ static DEFINE_PER_CPU(unsigned long, cpu_debugreg[HBP_NUM]);
*/
static DEFINE_PER_CPU(struct perf_event *, bp_per_reg[HBP_NUM]);
-
static inline unsigned long
__encode_dr7(int drnum, unsigned int len, unsigned int type)
{
@@ -86,96 +85,122 @@ int decode_dr7(unsigned long dr7, int bpnum, unsigned *len, unsigned *type)
}
/*
- * Install a perf counter breakpoint.
- *
- * We seek a free debug address register and use it for this
- * breakpoint. Eventually we enable it in the debug control register.
- *
- * Atomic: we hold the counter->ctx->lock and we only handle variables
- * and registers local to this cpu.
+ * We seek a slot and change it or keep it based on the action.
+ * Returns slot number on success, negative error on failure.
+ * Must be called with IRQs disabled.
*/
-int arch_install_hw_breakpoint(struct perf_event *bp)
+static int manage_bp_slot(struct perf_event *bp, enum bp_slot_action action)
{
- struct arch_hw_breakpoint *info = counter_arch_bp(bp);
- unsigned long *dr7;
- int i;
-
- lockdep_assert_irqs_disabled();
+ struct perf_event *old_bp;
+ struct perf_event *new_bp;
+ int slot;
+
+ switch (action) {
+ case BP_SLOT_ACTION_INSTALL:
+ old_bp = NULL;
+ new_bp = bp;
+ break;
+ case BP_SLOT_ACTION_UNINSTALL:
+ old_bp = bp;
+ new_bp = NULL;
+ break;
+ default:
+ return -EINVAL;
+ }
- for (i = 0; i < HBP_NUM; i++) {
- struct perf_event **slot = this_cpu_ptr(&bp_per_reg[i]);
+ for (slot = 0; slot < HBP_NUM; slot++) {
+ struct perf_event **curr = this_cpu_ptr(&bp_per_reg[slot]);
- if (!*slot) {
- *slot = bp;
- break;
+ if (*curr == old_bp) {
+ *curr = new_bp;
+ return slot;
}
}
- if (WARN_ONCE(i == HBP_NUM, "Can't find any breakpoint slot"))
- return -EBUSY;
+ if (old_bp) {
+ WARN_ONCE(1, "Can't find matching breakpoint slot");
+ return -EINVAL;
+ }
- set_debugreg(info->address, i);
- __this_cpu_write(cpu_debugreg[i], info->address);
+ WARN_ONCE(1, "No free breakpoint slots");
+ return -EBUSY;
+}
- dr7 = this_cpu_ptr(&cpu_dr7);
- *dr7 |= encode_dr7(i, info->len, info->type);
+static void setup_hwbp(struct arch_hw_breakpoint *info, int slot, bool enable)
+{
+ unsigned long dr7;
+
+ set_debugreg(info->address, slot);
+ __this_cpu_write(cpu_debugreg[slot], info->address);
+
+ dr7 = this_cpu_read(cpu_dr7);
+ if (enable)
+ dr7 |= encode_dr7(slot, info->len, info->type);
+ else
+ dr7 &= ~__encode_dr7(slot, 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.
+ * Enabling:
+ * 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.
*/
+ if (enable)
+ this_cpu_write(cpu_dr7, dr7);
+
barrier();
- set_debugreg(*dr7, 7);
- if (info->mask)
- amd_set_dr_addr_mask(info->mask, i);
+ set_debugreg(dr7, 7);
- return 0;
+ /*
+ * Always push the address mask, even when clearing it (info->mask == 0):
+ * a REINSTALL from a masked range breakpoint to an exact one must drop
+ * the stale mask, or the CPU keeps matching the wider range.
+ * amd_set_dr_addr_mask() is a no-op without X86_FEATURE_BPEXT and skips
+ * redundant MSR writes, so the unconditional call is cheap.
+ */
+ amd_set_dr_addr_mask(enable ? info->mask : 0, slot);
+
+ /*
+ * Disabling:
+ * Ensure the write to cpu_dr7 is after we've set the DR7 register.
+ * This ensures an NMI never see cpu_dr7 0 when DR7 is not.
+ * The barrier keeps the compiler from reordering the two: native
+ * set_debugreg() has no memory clobber of its own.
+ */
+ if (!enable) {
+ barrier();
+ this_cpu_write(cpu_dr7, dr7);
+ }
}
/*
- * Uninstall the breakpoint contained in the given counter.
- *
- * First we search the debug address register it uses and then we disable
- * it.
- *
- * Atomic: we hold the counter->ctx->lock and we only handle variables
- * and registers local to this cpu.
+ * find suitable breakpoint slot and set it up based on the action
*/
-void arch_uninstall_hw_breakpoint(struct perf_event *bp)
+static int arch_manage_bp(struct perf_event *bp, enum bp_slot_action action)
{
- struct arch_hw_breakpoint *info = counter_arch_bp(bp);
- unsigned long dr7;
- int i;
+ struct arch_hw_breakpoint *info;
+ int slot;
lockdep_assert_irqs_disabled();
- for (i = 0; i < HBP_NUM; i++) {
- struct perf_event **slot = this_cpu_ptr(&bp_per_reg[i]);
-
- if (*slot == bp) {
- *slot = NULL;
- break;
- }
- }
-
- if (WARN_ONCE(i == HBP_NUM, "Can't find any breakpoint slot"))
- return;
+ slot = manage_bp_slot(bp, action);
+ if (slot < 0)
+ return slot;
- dr7 = this_cpu_read(cpu_dr7);
- dr7 &= ~__encode_dr7(i, info->len, info->type);
+ info = counter_arch_bp(bp);
+ setup_hwbp(info, slot, action != BP_SLOT_ACTION_UNINSTALL);
- set_debugreg(dr7, 7);
- if (info->mask)
- amd_set_dr_addr_mask(0, i);
+ return 0;
+}
- /*
- * Ensure the write to cpu_dr7 is after we've set the DR7 register.
- * This ensures an NMI never see cpu_dr7 0 when DR7 is not.
- */
- barrier();
+int arch_install_hw_breakpoint(struct perf_event *bp)
+{
+ return arch_manage_bp(bp, BP_SLOT_ACTION_INSTALL);
+}
- this_cpu_write(cpu_dr7, dr7);
+void arch_uninstall_hw_breakpoint(struct perf_event *bp)
+{
+ arch_manage_bp(bp, BP_SLOT_ACTION_UNINSTALL);
}
static int arch_bp_generic_len(int x86_len)
--
2.53.0
^ permalink raw reply related [flat|nested] 24+ messages in thread* Re: [RFC PATCH v2 02/13] x86/hw_breakpoint: Unify breakpoint install/uninstall
2026-07-17 13:02 ` [RFC PATCH v2 02/13] x86/hw_breakpoint: Unify breakpoint install/uninstall Jinchao Wang
@ 2026-07-17 13:15 ` sashiko-bot
0 siblings, 0 replies; 24+ messages in thread
From: sashiko-bot @ 2026-07-17 13:15 UTC (permalink / raw)
To: Jinchao Wang; +Cc: linux-perf-users
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Medium] Unnecessary write to debug address registers (DR0-DR3) during breakpoint uninstall causes a performance regression.
--
--- Patch [2]: [RFC PATCH v2 02/13] x86/hw_breakpoint: Unify breakpoint install/uninstall ---
commit 5b286a3cc2eb7cc8f15c628fb198117f5bc1729d
Author: Jinchao Wang <wangjinchao600@gmail.com>
x86/hw_breakpoint: Unify breakpoint install/uninstall
This commit consolidates breakpoint management to reduce code duplication
by unifying the install and uninstall paths into common management functions.
It also includes linux/types.h to fix a missing build dependency.
> --- a/arch/x86/kernel/hw_breakpoint.c
> +++ b/arch/x86/kernel/hw_breakpoint.c
[ ... ]
> +static void setup_hwbp(struct arch_hw_breakpoint *info, int slot, bool enable)
> +{
> + unsigned long dr7;
> +
> + set_debugreg(info->address, slot);
> + __this_cpu_write(cpu_debugreg[slot], info->address);
[Severity: Medium]
Does this code introduce a performance regression by unconditionally writing to
the hardware debug address registers (DR0-DR3) during uninstallation?
Writing to hardware debug registers is an expensive operation that forces
pipeline flushes on bare metal and causes VM-exits in virtualized environments.
Looking at the old arch_uninstall_hw_breakpoint(), the uninstall path used to
only update DR7 and skipped writing the address register.
Now, when a task with a hardware breakpoint is scheduled out, the perf_event
subsystem uninstalls the breakpoint which calls into setup_hwbp() with
enable=false. This forces an unconditional set_debugreg() on the address
register anyway. Could this write be restricted to only happen when the enable
flag is true?
> +
> + dr7 = this_cpu_read(cpu_dr7);
> + if (enable)
> + dr7 |= encode_dr7(slot, info->len, info->type);
> + else
> + dr7 &= ~__encode_dr7(slot, info->len, info->type);
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260717125023.1895892-1-wangjinchao600@gmail.com?part=2
^ permalink raw reply [flat|nested] 24+ messages in thread
* [RFC PATCH v2 03/13] x86/hw_breakpoint: Add arch_reinstall_hw_breakpoint
2026-07-17 12:50 [RFC PATCH v2 00/13] mm/kwatch: dynamic hardware watchpoints for hunting memory corruption Jinchao Wang
2026-07-17 13:02 ` [RFC PATCH v2 01/13] arch: add HAVE_REINSTALL_HW_BREAKPOINT Jinchao Wang
2026-07-17 13:02 ` [RFC PATCH v2 02/13] x86/hw_breakpoint: Unify breakpoint install/uninstall Jinchao Wang
@ 2026-07-17 13:03 ` Jinchao Wang
2026-07-17 13:03 ` [RFC PATCH v2 04/13] HWBP: Add modify_wide_hw_breakpoint_local() API Jinchao Wang
` (10 subsequent siblings)
13 siblings, 0 replies; 24+ messages in thread
From: Jinchao Wang @ 2026-07-17 13:03 UTC (permalink / raw)
To: Andrew Morton, Peter Zijlstra, Thomas Gleixner, Steven Rostedt,
Masami Hiramatsu
Cc: Ingo Molnar, Borislav Petkov, Dave Hansen, H . Peter Anvin, x86,
Arnaldo Carvalho de Melo, Namhyung Kim, Mark Rutland,
Mathieu Desnoyers, David Hildenbrand, Jonathan Corbet,
Matthew Wilcox, Alan Stern, Randy Dunlap, Alexander Potapenko,
Marco Elver, Mike Rapoport, linux-kernel, linux-mm,
linux-trace-kernel, linux-perf-users, linux-doc, Jinchao Wang
The new arch_reinstall_hw_breakpoint() function can be used in an
atomic context, unlike the more expensive free and re-allocation path.
This allows callers to efficiently re-establish an existing breakpoint,
and x86 advertises the capability via HAVE_REINSTALL_HW_BREAKPOINT.
Since a REINSTALL may change bp_len, setup_hwbp() must clear the
slot's stale len/type and enable bits in DR7 before re-encoding:
OR-merging the new encoding over the old one would keep the CPU
watching with the stale width (verified in QEMU by reading DR7 after
re-arming watch_len=1 over a len8 breakpoint: 0x999906aa merged
without the clearing, 0x199906aa with it).
Signed-off-by: Jinchao Wang <wangjinchao600@gmail.com>
---
arch/x86/Kconfig | 1 +
arch/x86/include/asm/hw_breakpoint.h | 2 ++
arch/x86/kernel/hw_breakpoint.c | 16 ++++++++++++++--
3 files changed, 17 insertions(+), 2 deletions(-)
diff --git a/arch/x86/Kconfig b/arch/x86/Kconfig
index bdad90f210e4..5be698db0241 100644
--- a/arch/x86/Kconfig
+++ b/arch/x86/Kconfig
@@ -246,6 +246,7 @@ config X86
select HAVE_FUNCTION_TRACER
select HAVE_GCC_PLUGINS
select HAVE_HW_BREAKPOINT
+ select HAVE_REINSTALL_HW_BREAKPOINT
select HAVE_IOREMAP_PROT
select HAVE_IRQ_EXIT_ON_IRQ_STACK if X86_64
select HAVE_IRQ_TIME_ACCOUNTING
diff --git a/arch/x86/include/asm/hw_breakpoint.h b/arch/x86/include/asm/hw_breakpoint.h
index aa6adac6c3a2..c22cc4e87fc5 100644
--- a/arch/x86/include/asm/hw_breakpoint.h
+++ b/arch/x86/include/asm/hw_breakpoint.h
@@ -21,6 +21,7 @@ struct arch_hw_breakpoint {
enum bp_slot_action {
BP_SLOT_ACTION_INSTALL,
+ BP_SLOT_ACTION_REINSTALL,
BP_SLOT_ACTION_UNINSTALL,
};
@@ -65,6 +66,7 @@ extern int hw_breakpoint_exceptions_notify(struct notifier_block *unused,
int arch_install_hw_breakpoint(struct perf_event *bp);
+int arch_reinstall_hw_breakpoint(struct perf_event *bp);
void arch_uninstall_hw_breakpoint(struct perf_event *bp);
void hw_breakpoint_pmu_read(struct perf_event *bp);
void hw_breakpoint_pmu_unthrottle(struct perf_event *bp);
diff --git a/arch/x86/kernel/hw_breakpoint.c b/arch/x86/kernel/hw_breakpoint.c
index 76886467708b..e2dbd43d8e39 100644
--- a/arch/x86/kernel/hw_breakpoint.c
+++ b/arch/x86/kernel/hw_breakpoint.c
@@ -100,6 +100,10 @@ static int manage_bp_slot(struct perf_event *bp, enum bp_slot_action action)
old_bp = NULL;
new_bp = bp;
break;
+ case BP_SLOT_ACTION_REINSTALL:
+ old_bp = bp;
+ new_bp = bp;
+ break;
case BP_SLOT_ACTION_UNINSTALL:
old_bp = bp;
new_bp = NULL;
@@ -134,10 +138,13 @@ static void setup_hwbp(struct arch_hw_breakpoint *info, int slot, bool enable)
__this_cpu_write(cpu_debugreg[slot], info->address);
dr7 = this_cpu_read(cpu_dr7);
+ /*
+ * Clear the slot's stale len/type and enable bits first: a REINSTALL
+ * with a different bp_len would otherwise OR-merge both encodings.
+ */
+ dr7 &= ~__encode_dr7(slot, 0xf, 0);
if (enable)
dr7 |= encode_dr7(slot, info->len, info->type);
- else
- dr7 &= ~__encode_dr7(slot, info->len, info->type);
/*
* Enabling:
@@ -198,6 +205,11 @@ int arch_install_hw_breakpoint(struct perf_event *bp)
return arch_manage_bp(bp, BP_SLOT_ACTION_INSTALL);
}
+int arch_reinstall_hw_breakpoint(struct perf_event *bp)
+{
+ return arch_manage_bp(bp, BP_SLOT_ACTION_REINSTALL);
+}
+
void arch_uninstall_hw_breakpoint(struct perf_event *bp)
{
arch_manage_bp(bp, BP_SLOT_ACTION_UNINSTALL);
--
2.53.0
^ permalink raw reply related [flat|nested] 24+ messages in thread* [RFC PATCH v2 04/13] HWBP: Add modify_wide_hw_breakpoint_local() API
2026-07-17 12:50 [RFC PATCH v2 00/13] mm/kwatch: dynamic hardware watchpoints for hunting memory corruption Jinchao Wang
` (2 preceding siblings ...)
2026-07-17 13:03 ` [RFC PATCH v2 03/13] x86/hw_breakpoint: Add arch_reinstall_hw_breakpoint Jinchao Wang
@ 2026-07-17 13:03 ` Jinchao Wang
2026-07-17 13:21 ` sashiko-bot
2026-07-17 13:04 ` [RFC PATCH v2 05/13] mm/kwatch: add watch expression parser and dereference engine Jinchao Wang
` (9 subsequent siblings)
13 siblings, 1 reply; 24+ messages in thread
From: Jinchao Wang @ 2026-07-17 13:03 UTC (permalink / raw)
To: Andrew Morton, Peter Zijlstra, Thomas Gleixner, Steven Rostedt,
Masami Hiramatsu
Cc: Ingo Molnar, Borislav Petkov, Dave Hansen, H . Peter Anvin, x86,
Arnaldo Carvalho de Melo, Namhyung Kim, Mark Rutland,
Mathieu Desnoyers, David Hildenbrand, Jonathan Corbet,
Matthew Wilcox, Alan Stern, Randy Dunlap, Alexander Potapenko,
Marco Elver, Mike Rapoport, linux-kernel, linux-mm,
linux-trace-kernel, linux-perf-users, linux-doc, Jinchao Wang
From: "Masami Hiramatsu (Google)" <mhiramat@kernel.org>
Add modify_wide_hw_breakpoint_local() arch-wide interface which allows
hwbp users to update watch address on-line. This is available if the
arch supports CONFIG_HAVE_REINSTALL_HW_BREAKPOINT.
Note that this allows to change the type only for compatible types,
because it does not release and reserve the hwbp slot based on type.
For instance, you can not change HW_BREAKPOINT_W to HW_BREAKPOINT_X.
Assisted-by: Antigravity:gemini-3.5-flash
Signed-off-by: Masami Hiramatsu (Google) <mhiramat@kernel.org>
Signed-off-by: Jinchao Wang <wangjinchao600@gmail.com>
---
include/linux/hw_breakpoint.h | 6 +++++
kernel/events/hw_breakpoint.c | 43 +++++++++++++++++++++++++++++++++++
2 files changed, 49 insertions(+)
diff --git a/include/linux/hw_breakpoint.h b/include/linux/hw_breakpoint.h
index db199d653dd1..6754ffbee9ed 100644
--- a/include/linux/hw_breakpoint.h
+++ b/include/linux/hw_breakpoint.h
@@ -81,6 +81,9 @@ register_wide_hw_breakpoint(struct perf_event_attr *attr,
perf_overflow_handler_t triggered,
void *context);
+extern int modify_wide_hw_breakpoint_local(struct perf_event *bp,
+ struct perf_event_attr *attr);
+
extern int register_perf_hw_breakpoint(struct perf_event *bp);
extern void unregister_hw_breakpoint(struct perf_event *bp);
extern void unregister_wide_hw_breakpoint(struct perf_event * __percpu *cpu_events);
@@ -124,6 +127,9 @@ register_wide_hw_breakpoint(struct perf_event_attr *attr,
perf_overflow_handler_t triggered,
void *context) { return NULL; }
static inline int
+modify_wide_hw_breakpoint_local(struct perf_event *bp,
+ struct perf_event_attr *attr) { return -EOPNOTSUPP; }
+static inline int
register_perf_hw_breakpoint(struct perf_event *bp) { return -ENOSYS; }
static inline void unregister_hw_breakpoint(struct perf_event *bp) { }
static inline void
diff --git a/kernel/events/hw_breakpoint.c b/kernel/events/hw_breakpoint.c
index 789add0c185a..f4709c892d67 100644
--- a/kernel/events/hw_breakpoint.c
+++ b/kernel/events/hw_breakpoint.c
@@ -888,6 +888,49 @@ void unregister_wide_hw_breakpoint(struct perf_event * __percpu *cpu_events)
}
EXPORT_SYMBOL_GPL(unregister_wide_hw_breakpoint);
+/**
+ * modify_wide_hw_breakpoint_local - update breakpoint config for local CPU
+ * @bp: the hwbp perf event for this CPU
+ * @attr: the new attribute for @bp
+ *
+ * This does not release and reserve the slot of a HWBP; it just reuses the
+ * current slot on local CPU. So the users must update the other CPUs by
+ * themselves.
+ * Also, since this does not release/reserve the slot, this can not change the
+ * type to incompatible type of the HWBP.
+ * Return err if attr is invalid or the CPU fails to update debug register
+ * for new @attr.
+ */
+#ifdef CONFIG_HAVE_REINSTALL_HW_BREAKPOINT
+int modify_wide_hw_breakpoint_local(struct perf_event *bp,
+ struct perf_event_attr *attr)
+{
+ struct arch_hw_breakpoint info;
+ int ret;
+
+ if (find_slot_idx(bp->attr.bp_type) != find_slot_idx(attr->bp_type))
+ return -EINVAL;
+
+ ret = hw_breakpoint_arch_parse(bp, attr, &info);
+ if (ret)
+ return ret;
+
+ *counter_arch_bp(bp) = info;
+ bp->attr.bp_addr = attr->bp_addr;
+ bp->attr.bp_type = attr->bp_type;
+ bp->attr.bp_len = attr->bp_len;
+
+ return arch_reinstall_hw_breakpoint(bp);
+}
+#else
+int modify_wide_hw_breakpoint_local(struct perf_event *bp,
+ struct perf_event_attr *attr)
+{
+ return -EOPNOTSUPP;
+}
+#endif
+EXPORT_SYMBOL_GPL(modify_wide_hw_breakpoint_local);
+
/**
* hw_breakpoint_is_used - check if breakpoints are currently used
*
--
2.53.0
^ permalink raw reply related [flat|nested] 24+ messages in thread* Re: [RFC PATCH v2 04/13] HWBP: Add modify_wide_hw_breakpoint_local() API
2026-07-17 13:03 ` [RFC PATCH v2 04/13] HWBP: Add modify_wide_hw_breakpoint_local() API Jinchao Wang
@ 2026-07-17 13:21 ` sashiko-bot
0 siblings, 0 replies; 24+ messages in thread
From: sashiko-bot @ 2026-07-17 13:21 UTC (permalink / raw)
To: Jinchao Wang; +Cc: linux-perf-users
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [High] Non-atomic updates to active hardware breakpoint state can cause torn reads in NMI/#DB handlers.
- [High] The function alters the event's internal software state before verifying if the hardware update succeeds, leaving the state permanently corrupted if the hardware update fails.
--
--- Patch [4]: [RFC PATCH v2 04/13] HWBP: Add modify_wide_hw_breakpoint_local() API ---
commit 326c7d6d8fc03bc42d6719968b050227e4e25f31
Author: Masami Hiramatsu (Google) <mhiramat@kernel.org>
HWBP: Add modify_wide_hw_breakpoint_local() API
This commit adds modify_wide_hw_breakpoint_local(), an arch-wide interface
allowing hardware breakpoint users to update watch addresses on-line for the
local CPU. It updates the configuration by reusing the current hardware slot
without releasing and reserving it.
> diff --git a/kernel/events/hw_breakpoint.c b/kernel/events/hw_breakpoint.c
> index 789add0c185a0..f4709c892d674 100644
> --- a/kernel/events/hw_breakpoint.c
> +++ b/kernel/events/hw_breakpoint.c
[ ... ]
> +int modify_wide_hw_breakpoint_local(struct perf_event *bp,
> + struct perf_event_attr *attr)
> +{
> + struct arch_hw_breakpoint info;
> + int ret;
> +
> + if (find_slot_idx(bp->attr.bp_type) != find_slot_idx(attr->bp_type))
> + return -EINVAL;
> +
> + ret = hw_breakpoint_arch_parse(bp, attr, &info);
> + if (ret)
> + return ret;
> +
> + *counter_arch_bp(bp) = info;
> + bp->attr.bp_addr = attr->bp_addr;
> + bp->attr.bp_type = attr->bp_type;
> + bp->attr.bp_len = attr->bp_len;
[Severity: High]
Can this cause a torn read if an NMI/#DB exception occurs while we are
non-atomically updating the breakpoint state?
If an NMI triggers a #DB exception before arch_reinstall_hw_breakpoint()
completes, the hw_breakpoint_handler might read the partially updated
bp->hw.info and bp->attr while the hardware DR7 register is still armed with
the old configuration.
On architectures like x86, reading a torn bp->hw.info.type might cause the
handler to misidentify the breakpoint type and fail to set the RF flag,
leading to an infinite #DB loop.
> +
> + return arch_reinstall_hw_breakpoint(bp);
[Severity: High]
Does this leave the software state permanently corrupted if the hardware
update fails?
If the PMU rotates the event out just before this function is called,
arch_reinstall_hw_breakpoint() might fail and return an error like -EINVAL.
This error is propagated to the caller, so the caller assumes the
modification failed.
However, the software state (bp->attr and bp->hw.info) was already
overwritten above and is not rolled back. When the PMU later rotates the
breakpoint back in, will it silently use this uncommitted new configuration?
> +}
> +#else
> +int modify_wide_hw_breakpoint_local(struct perf_event *bp,
> + struct perf_event_attr *attr)
> +{
> + return -EOPNOTSUPP;
> +}
> +#endif
> +EXPORT_SYMBOL_GPL(modify_wide_hw_breakpoint_local);
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260717125023.1895892-1-wangjinchao600@gmail.com?part=4
^ permalink raw reply [flat|nested] 24+ messages in thread
* [RFC PATCH v2 05/13] mm/kwatch: add watch expression parser and dereference engine
2026-07-17 12:50 [RFC PATCH v2 00/13] mm/kwatch: dynamic hardware watchpoints for hunting memory corruption Jinchao Wang
` (3 preceding siblings ...)
2026-07-17 13:03 ` [RFC PATCH v2 04/13] HWBP: Add modify_wide_hw_breakpoint_local() API Jinchao Wang
@ 2026-07-17 13:04 ` Jinchao Wang
2026-07-17 13:15 ` sashiko-bot
2026-07-17 13:04 ` [RFC PATCH v2 06/13] mm/kwatch: add lockless per-task context pool Jinchao Wang
` (8 subsequent siblings)
13 siblings, 1 reply; 24+ messages in thread
From: Jinchao Wang @ 2026-07-17 13:04 UTC (permalink / raw)
To: Andrew Morton, Peter Zijlstra, Thomas Gleixner, Steven Rostedt,
Masami Hiramatsu
Cc: Ingo Molnar, Borislav Petkov, Dave Hansen, H . Peter Anvin, x86,
Arnaldo Carvalho de Melo, Namhyung Kim, Mark Rutland,
Mathieu Desnoyers, David Hildenbrand, Jonathan Corbet,
Matthew Wilcox, Alan Stern, Randy Dunlap, Alexander Potapenko,
Marco Elver, Mike Rapoport, linux-kernel, linux-mm,
linux-trace-kernel, linux-perf-users, linux-doc, Jinchao Wang
KWatch watches a memory address that is only known once the target
function runs, e.g. "argument 1, plus 8, dereferenced once". Add the
two halves of that mechanism:
- kwatch_deref_parse() turns a textual watch expression
{base}[+-off][->[+-]off]... into a kwatch_config: a base anchor
(arg1..arg6, stack, an absolute address or - for built-in KWatch -
a symbol name) plus a static offset chain.
- kwatch_deref_resolve() replays the chain at probe time against
pt_regs. Every pointer load goes through get_kernel_nofault() and
the final address must be a kernel address.
Also add the internal kwatch.h header shared by the rest of the
series. Nothing is built yet; the Kconfig entry comes with the
control plane.
Signed-off-by: Jinchao Wang <wangjinchao600@gmail.com>
---
mm/kwatch/Makefile | 3 +
mm/kwatch/deref.c | 174 +++++++++++++++++++++++++++++++++++++++++++++
mm/kwatch/kwatch.h | 101 ++++++++++++++++++++++++++
3 files changed, 278 insertions(+)
create mode 100644 mm/kwatch/Makefile
create mode 100644 mm/kwatch/deref.c
create mode 100644 mm/kwatch/kwatch.h
diff --git a/mm/kwatch/Makefile b/mm/kwatch/Makefile
new file mode 100644
index 000000000000..69c21ae62123
--- /dev/null
+++ b/mm/kwatch/Makefile
@@ -0,0 +1,3 @@
+obj-$(CONFIG_KWATCH) += kwatch.o
+
+kwatch-y := deref.o
diff --git a/mm/kwatch/deref.c b/mm/kwatch/deref.c
new file mode 100644
index 000000000000..a93c76139e7c
--- /dev/null
+++ b/mm/kwatch/deref.c
@@ -0,0 +1,174 @@
+// SPDX-License-Identifier: GPL-2.0
+#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
+
+#include <linux/ptrace.h>
+#include <linux/sched.h>
+#include <linux/uaccess.h>
+#include <linux/kallsyms.h>
+#include <linux/string.h>
+#include <linux/slab.h>
+
+#include "kwatch.h"
+
+int kwatch_deref_resolve(const struct kwatch_config *cfg, struct pt_regs *regs,
+ unsigned long *out_addr, u16 *out_len)
+{
+ unsigned long addr = 0;
+ int i;
+
+ /* 1. Resolve the Base Anchor */
+ if (cfg->base == KWATCH_BASE_STACK) {
+ addr = kernel_stack_pointer(regs);
+ if (unlikely(!addr))
+ return -EINVAL;
+ } else if (cfg->base >= KWATCH_BASE_ARG1 &&
+ cfg->base <= KWATCH_BASE_ARG6) {
+ int arg_idx = cfg->base - KWATCH_BASE_ARG1;
+
+ addr = regs_get_kernel_argument(regs, arg_idx);
+ } else if (cfg->base == KWATCH_BASE_ABS_ADDR ||
+ cfg->base == KWATCH_BASE_GLOBAL_SYM) {
+ /* Zero-latency load of the static symbol location */
+ addr = cfg->sym_addr;
+ } else {
+ return -EINVAL;
+ }
+
+ /* 2. The Pointer-Chasing FSM */
+ for (i = 0; i < cfg->offset_count; i++) {
+ addr += cfg->offsets[i];
+
+ if (i < cfg->offset_count - 1) {
+ unsigned long next_addr;
+
+ /* Dynamically read the pointer contents at runtime */
+ if (get_kernel_nofault(next_addr, (unsigned long *)addr))
+ return -EFAULT;
+
+ addr = next_addr;
+ }
+ }
+
+ /* Enforce strict Kernel-Space boundary */
+ if (unlikely(addr < TASK_SIZE_MAX))
+ return -EINVAL;
+
+ *out_addr = addr;
+ *out_len = cfg->watch_len;
+ return 0;
+}
+
+int kwatch_deref_parse(struct kwatch_config *cfg, const char *watch_expr)
+{
+ char *p, *sep, *dup_expr;
+ char type = '\0';
+ bool is_deref = false;
+ int ret = 0;
+
+ dup_expr = kstrdup(watch_expr, GFP_KERNEL);
+ if (!dup_expr)
+ return -ENOMEM;
+
+ cfg->offset_count = 1;
+ cfg->offsets[0] = 0;
+
+ /* 1. Isolate and Resolve Base Anchor */
+ p = dup_expr;
+ sep = NULL;
+ while (*p) {
+ if (*p == '+') {
+ sep = p;
+ type = '+';
+ break;
+ }
+ if (*p == '-') {
+ sep = p;
+ type = '-';
+ if (p[1] == '>')
+ is_deref = true;
+ break;
+ }
+ p++;
+ }
+
+ if (type)
+ *sep = '\0';
+
+ if (!strcmp(dup_expr, "stack")) {
+ cfg->base = KWATCH_BASE_STACK;
+ } else if (!strncmp(dup_expr, "arg", 3) && strlen(dup_expr) == 4) {
+ int arg_num;
+
+ if (kstrtoint(dup_expr + 3, 10, &arg_num) || arg_num < 1 ||
+ arg_num > 6) {
+ ret = -EINVAL;
+ goto out;
+ }
+ cfg->base = KWATCH_BASE_ARG1 + (arg_num - 1);
+ } else if (kstrtoul(dup_expr, 0, &cfg->sym_addr) == 0) {
+ cfg->base = KWATCH_BASE_ABS_ADDR;
+ } else {
+#if IS_BUILTIN(CONFIG_KWATCH)
+ cfg->sym_addr = kallsyms_lookup_name(dup_expr);
+ if (!cfg->sym_addr) {
+ pr_err("Failed to resolve symbol name: %s\n", dup_expr);
+ ret = -EINVAL;
+ goto out;
+ }
+ cfg->base = KWATCH_BASE_GLOBAL_SYM;
+#else
+ pr_err("cannot resolve symbol %s when built as a module, use a hex address\n",
+ dup_expr);
+ ret = -EINVAL;
+ goto out;
+#endif
+ }
+
+ if (!type)
+ goto out;
+
+ /* 2. Resolve Base Offset (if + or - exists) */
+ if (!is_deref) {
+ char *next;
+
+ *sep = type; /* Restore the '+' or '-' for kstrtol */
+ next = strstr(sep, "->");
+ if (next)
+ *next = '\0';
+
+ if (kstrtol(sep, 0, &cfg->offsets[0])) {
+ ret = -EINVAL;
+ goto out;
+ }
+
+ p = next ? next + 2 : NULL;
+ } else {
+ /* Jump directly to the first dereference after '->' */
+ p = sep + 2;
+ }
+
+ /* 3. Resolve Dereference Chain */
+ while (p) {
+ char *next;
+
+ if (cfg->offset_count >= MAX_DEREF_CHAIN) {
+ ret = -E2BIG;
+ goto out;
+ }
+
+ next = strstr(p, "->");
+ if (next)
+ *next = '\0';
+
+ if (kstrtol(p, 0, &cfg->offsets[cfg->offset_count++])) {
+ ret = -EINVAL;
+ goto out;
+ }
+
+ p = next ? next + 2 : NULL;
+ }
+
+out:
+ kfree(dup_expr);
+ return ret;
+}
diff --git a/mm/kwatch/kwatch.h b/mm/kwatch/kwatch.h
new file mode 100644
index 000000000000..dbe0fd0e6a0d
--- /dev/null
+++ b/mm/kwatch/kwatch.h
@@ -0,0 +1,101 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+#ifndef _MM_KWATCH_H
+#define _MM_KWATCH_H
+
+#include <linux/fprobe.h>
+#include <linux/kprobes.h>
+#include <linux/perf_event.h>
+#include <linux/sched.h>
+#include <linux/types.h>
+#include <linux/compiler.h>
+#include <linux/atomic.h>
+
+#define MAX_CONFIG_STR_LEN 512
+#define MAX_DEREF_CHAIN 4
+
+struct kwatch_watchpoint;
+
+struct kwatch_tsk_ctx {
+ struct task_struct *task;
+ struct kwatch_watchpoint *wp;
+ u16 depth;
+ u32 epoch;
+};
+
+struct kwatch_watchpoint {
+ struct perf_event *__percpu *event;
+ call_single_data_t __percpu *csd_arm;
+ call_single_data_t __percpu *csd_disarm;
+ struct perf_event_attr attr;
+ atomic_t in_use; // multi-consumer safe get/put
+ struct list_head list; // for cpu online and offline
+
+ struct task_struct *arm_tsk;
+ atomic_t pending_ipis;
+ atomic_t refcount;
+ bool teardown;
+};
+
+enum kwatch_base_type {
+ KWATCH_BASE_STACK,
+ KWATCH_BASE_ABS_ADDR,
+ KWATCH_BASE_GLOBAL_SYM,
+ KWATCH_BASE_ARG1,
+ KWATCH_BASE_ARG2,
+ KWATCH_BASE_ARG3,
+ KWATCH_BASE_ARG4,
+ KWATCH_BASE_ARG5,
+ KWATCH_BASE_ARG6,
+};
+
+struct kwatch_config {
+ u16 max_watch;
+ char func_name[KSYM_NAME_LEN];
+ u16 func_offset;
+ u16 depth;
+ u16 duration;
+ u16 watch_len;
+
+ /* Unified Deref Engine State */
+ enum kwatch_base_type base;
+ char watch_expr[MAX_CONFIG_STR_LEN];
+ unsigned long sym_addr;
+ long offsets[MAX_DEREF_CHAIN];
+ u8 offset_count;
+ u16 max_concurrency;
+};
+
+int kwatch_hwbp_prealloc(u16 max_watch);
+void kwatch_hwbp_free(void);
+int kwatch_hwbp_get(struct kwatch_watchpoint **out_wp);
+void kwatch_hwbp_arm(struct kwatch_watchpoint *wp, unsigned long addr, u16 len);
+int kwatch_hwbp_put(struct kwatch_watchpoint *wp);
+
+int kwatch_probe_start(struct kwatch_config *cfg);
+void kwatch_probe_stop(void);
+void kwatch_probe_mute(bool mute);
+bool kwatch_probe_validate_hit(struct pt_regs *regs, struct task_struct *arm_tsk);
+unsigned long kwatch_probe_nmi_rejected(void);
+unsigned long kwatch_hwbp_arm_ipi_suppressed(void);
+
+int kwatch_tsk_ctx_prealloc(u16 max_concurrency);
+struct kwatch_tsk_ctx *kwatch_tsk_ctx_get(bool can_alloc);
+void kwatch_tsk_ctx_put(void);
+void kwatch_tsk_ctx_release(struct kwatch_tsk_ctx *ctx);
+void kwatch_tsk_ctx_reset(struct kwatch_tsk_ctx *ctx, u32 new_epoch);
+void kwatch_tsk_ctx_release_wps(void);
+void kwatch_tsk_ctx_free(void);
+
+void kwatch_global_anchor(unsigned long duration_sec);
+int kwatch_anchor_start(u16 duration);
+void kwatch_anchor_stop(void);
+void kwatch_anchor_cancel_work(void);
+bool kwatch_anchor_has_expired(void);
+void kwatch_anchor_clear_expired(void);
+void kwatch_auto_stop(void);
+
+int kwatch_deref_resolve(const struct kwatch_config *cfg, struct pt_regs *regs,
+ unsigned long *out_addr, u16 *out_len);
+int kwatch_deref_parse(struct kwatch_config *cfg, const char *watch_expr);
+
+#endif /* _MM_KWATCH_H */
--
2.53.0
^ permalink raw reply related [flat|nested] 24+ messages in thread* Re: [RFC PATCH v2 05/13] mm/kwatch: add watch expression parser and dereference engine
2026-07-17 13:04 ` [RFC PATCH v2 05/13] mm/kwatch: add watch expression parser and dereference engine Jinchao Wang
@ 2026-07-17 13:15 ` sashiko-bot
0 siblings, 0 replies; 24+ messages in thread
From: sashiko-bot @ 2026-07-17 13:15 UTC (permalink / raw)
To: Jinchao Wang; +Cc: linux-perf-users
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Medium] The watch expression parser incorrectly shadows valid 4-character symbol names starting with 'arg' (e.g., 'args', 'argv', 'argc', 'arg0').
--
--- Patch [5]: [RFC PATCH v2 05/13] mm/kwatch: add watch expression parser and dereference engine ---
commit f73ceb00166a670b40222eb3b2e46fa6d5931306
Author: Jinchao Wang <wangjinchao600@gmail.com>
mm/kwatch: add watch expression parser and dereference engine
This commit introduces the watch expression parser and dereference engine for
KWatch. It allows parsing textual expressions into configurations and resolving
them safely against pt_regs at probe time.
> diff --git a/mm/kwatch/deref.c b/mm/kwatch/deref.c
> new file mode 100644
> index 0000000000000..a93c76139e7cd
> --- /dev/null
> +++ b/mm/kwatch/deref.c
[ ... ]
> +int kwatch_deref_parse(struct kwatch_config *cfg, const char *watch_expr)
> +{
[ ... ]
> + if (!strcmp(dup_expr, "stack")) {
> + cfg->base = KWATCH_BASE_STACK;
> + } else if (!strncmp(dup_expr, "arg", 3) && strlen(dup_expr) == 4) {
> + int arg_num;
> +
> + if (kstrtoint(dup_expr + 3, 10, &arg_num) || arg_num < 1 ||
> + arg_num > 6) {
> + ret = -EINVAL;
> + goto out;
> + }
[Severity: Medium]
Will this logic incorrectly reject valid 4-character symbol names that start
with "arg" (like "args", "argv", or "arg0")?
If a user attempts to watch a global symbol like "argv", this block intercepts
it because it starts with "arg" and is 4 characters long. However, the call to
kstrtoint() will fail on the "v" character, causing the parser to return
-EINVAL immediately instead of falling through to the general symbol lookup
block.
> + cfg->base = KWATCH_BASE_ARG1 + (arg_num - 1);
> + } else if (kstrtoul(dup_expr, 0, &cfg->sym_addr) == 0) {
> + cfg->base = KWATCH_BASE_ABS_ADDR;
> + } else {
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260717125023.1895892-1-wangjinchao600@gmail.com?part=5
^ permalink raw reply [flat|nested] 24+ messages in thread
* [RFC PATCH v2 06/13] mm/kwatch: add lockless per-task context pool
2026-07-17 12:50 [RFC PATCH v2 00/13] mm/kwatch: dynamic hardware watchpoints for hunting memory corruption Jinchao Wang
` (4 preceding siblings ...)
2026-07-17 13:04 ` [RFC PATCH v2 05/13] mm/kwatch: add watch expression parser and dereference engine Jinchao Wang
@ 2026-07-17 13:04 ` Jinchao Wang
2026-07-17 13:19 ` sashiko-bot
2026-07-17 13:04 ` [RFC PATCH v2 07/13] stacktrace: export stack_trace_save_regs() Jinchao Wang
` (7 subsequent siblings)
13 siblings, 1 reply; 24+ messages in thread
From: Jinchao Wang @ 2026-07-17 13:04 UTC (permalink / raw)
To: Andrew Morton, Peter Zijlstra, Thomas Gleixner, Steven Rostedt,
Masami Hiramatsu
Cc: Ingo Molnar, Borislav Petkov, Dave Hansen, H . Peter Anvin, x86,
Arnaldo Carvalho de Melo, Namhyung Kim, Mark Rutland,
Mathieu Desnoyers, David Hildenbrand, Jonathan Corbet,
Matthew Wilcox, Alan Stern, Randy Dunlap, Alexander Potapenko,
Marco Elver, Mike Rapoport, linux-kernel, linux-mm,
linux-trace-kernel, linux-perf-users, linux-doc, Jinchao Wang
A task that enters the watched function needs somewhere to keep its
window state (nesting depth, owned watchpoint, config epoch). The
lookup runs in kprobe and NMI-like contexts, so it must not allocate
or take locks.
Use a preallocated open-addressing array hashed by task_struct
pointer. Slots are claimed with cmpxchg() and released with
smp_store_release(); lookup is a read-only probe sequence. The pool
size (max_concurrency) bounds how many tasks can be inside watch
windows concurrently; excess tasks are simply not tracked.
Signed-off-by: Jinchao Wang <wangjinchao600@gmail.com>
---
mm/kwatch/Makefile | 2 +-
mm/kwatch/task_ctx.c | 125 +++++++++++++++++++++++++++++++++++++++++++
2 files changed, 126 insertions(+), 1 deletion(-)
create mode 100644 mm/kwatch/task_ctx.c
diff --git a/mm/kwatch/Makefile b/mm/kwatch/Makefile
index 69c21ae62123..cc6574df0d68 100644
--- a/mm/kwatch/Makefile
+++ b/mm/kwatch/Makefile
@@ -1,3 +1,3 @@
obj-$(CONFIG_KWATCH) += kwatch.o
-kwatch-y := deref.o
+kwatch-y := deref.o task_ctx.o
diff --git a/mm/kwatch/task_ctx.c b/mm/kwatch/task_ctx.c
new file mode 100644
index 000000000000..64383a4429e7
--- /dev/null
+++ b/mm/kwatch/task_ctx.c
@@ -0,0 +1,125 @@
+// SPDX-License-Identifier: GPL-2.0
+#include <linux/slab.h>
+#include <linux/hash.h>
+#include <linux/sched.h>
+#include <linux/log2.h>
+#include "kwatch.h"
+
+static u16 kwatch_ctx_pool_size;
+static u16 kwatch_ctx_pool_mask;
+
+static struct kwatch_tsk_ctx *kwatch_ctx_pool;
+
+/* Pool size is a u16 and indexes a power-of-two hash table, so bound the
+ * request away from both the roundup_pow_of_two() u16 overflow (>32768 wraps
+ * to 0) and the degenerate size-1 case (ilog2(1) == 0 breaks hash_ptr()).
+ */
+#define KWATCH_CTX_POOL_MIN 256
+#define KWATCH_CTX_POOL_MAX 32768
+
+int kwatch_tsk_ctx_prealloc(u16 max_concurrency)
+{
+ if (max_concurrency < KWATCH_CTX_POOL_MIN)
+ max_concurrency = KWATCH_CTX_POOL_MIN;
+ else if (max_concurrency > KWATCH_CTX_POOL_MAX)
+ max_concurrency = KWATCH_CTX_POOL_MAX;
+
+ /*
+ * Set the size/mask only when actually allocating, so they can never
+ * drift out of sync with the live pool if prealloc is ever called
+ * again without a matching free.
+ */
+ if (unlikely(!kwatch_ctx_pool)) {
+ kwatch_ctx_pool_size = roundup_pow_of_two(max_concurrency);
+ kwatch_ctx_pool_mask = kwatch_ctx_pool_size - 1;
+
+ kwatch_ctx_pool = kcalloc(kwatch_ctx_pool_size,
+ sizeof(struct kwatch_tsk_ctx),
+ GFP_KERNEL);
+ if (!kwatch_ctx_pool)
+ return -ENOMEM;
+ }
+ return 0;
+}
+
+struct kwatch_tsk_ctx *kwatch_tsk_ctx_get(bool can_alloc)
+{
+ int start_idx, i, idx;
+ struct task_struct *t;
+
+ if (unlikely(!kwatch_ctx_pool))
+ return NULL;
+
+ start_idx = hash_ptr(current, ilog2(kwatch_ctx_pool_size));
+
+ for (i = 0; i < kwatch_ctx_pool_size; i++) {
+ idx = (start_idx + i) & kwatch_ctx_pool_mask;
+ t = READ_ONCE(kwatch_ctx_pool[idx].task);
+ if (t == current)
+ return &kwatch_ctx_pool[idx];
+ }
+
+ if (!can_alloc)
+ return NULL;
+
+ for (i = 0; i < kwatch_ctx_pool_size; i++) {
+ idx = (start_idx + i) & kwatch_ctx_pool_mask;
+ t = READ_ONCE(kwatch_ctx_pool[idx].task);
+ if (!t) {
+ if (!cmpxchg(&kwatch_ctx_pool[idx].task, NULL, current))
+ return &kwatch_ctx_pool[idx];
+ }
+ }
+
+ return NULL;
+}
+
+void kwatch_tsk_ctx_reset(struct kwatch_tsk_ctx *ctx, u32 new_epoch)
+{
+ struct kwatch_watchpoint *wp = xchg(&ctx->wp, NULL);
+
+ if (wp)
+ kwatch_hwbp_put(wp);
+ ctx->depth = 0;
+ ctx->epoch = new_epoch;
+}
+
+/* Release a slot we hold a pointer to: disarm its wp and free the slot. */
+void kwatch_tsk_ctx_release(struct kwatch_tsk_ctx *ctx)
+{
+ kwatch_tsk_ctx_reset(ctx, 0);
+
+ /* Pairs with READ_ONCE() in kwatch_tsk_ctx_get() */
+ smp_store_release(&ctx->task, NULL);
+}
+
+void kwatch_tsk_ctx_put(void)
+{
+ struct kwatch_tsk_ctx *ctx = kwatch_tsk_ctx_get(false);
+
+ if (unlikely(!ctx))
+ return;
+
+ kwatch_tsk_ctx_release(ctx);
+}
+
+void kwatch_tsk_ctx_release_wps(void)
+{
+ int i;
+
+ if (!kwatch_ctx_pool)
+ return;
+
+ for (i = 0; i < kwatch_ctx_pool_size; i++) {
+ struct kwatch_watchpoint *wp = xchg(&kwatch_ctx_pool[i].wp,
+ NULL);
+ if (wp)
+ kwatch_hwbp_put(wp);
+ }
+}
+
+void kwatch_tsk_ctx_free(void)
+{
+ kfree(kwatch_ctx_pool);
+ kwatch_ctx_pool = NULL;
+}
--
2.53.0
^ permalink raw reply related [flat|nested] 24+ messages in thread* Re: [RFC PATCH v2 06/13] mm/kwatch: add lockless per-task context pool
2026-07-17 13:04 ` [RFC PATCH v2 06/13] mm/kwatch: add lockless per-task context pool Jinchao Wang
@ 2026-07-17 13:19 ` sashiko-bot
0 siblings, 0 replies; 24+ messages in thread
From: sashiko-bot @ 2026-07-17 13:19 UTC (permalink / raw)
To: Jinchao Wang; +Cc: linux-perf-users
Thank you for your contribution! Sashiko AI review found 6 potential issue(s) to consider:
- [Critical] Concurrent lockless accesses to the global `kwatch_ctx_pool` lack RCU or READ_ONCE protection, leading to Use-After-Free or NULL pointer dereferences.
- [High] NMI reentrancy between slot claim and release causes premature release, double free, and cross-task data corruption.
- [High] Hash table lookup degenerates to an unconditional O(N) full array scan on negative lookups, causing massive NMI latency.
- [High] Context slots permanently leak if a task exits abruptly, causing state corruption and ABA problems when the task_struct is reallocated.
- [High] Missing release/acquire memory barriers when dynamically publishing `kwatch_ctx_pool` expose lockless readers to uninitialized state and UB.
- [Medium] Large contiguous memory allocation with kcalloc() risks frequent failure on fragmented systems.
--
--- Patch [6]: [RFC PATCH v2 06/13] mm/kwatch: add lockless per-task context pool ---
commit 48d8ef7e220842e6317681d38fcc2886436cae7d
Author: Jinchao Wang <wangjinchao600@gmail.com>
mm/kwatch: add lockless per-task context pool
A task that enters the watched function needs somewhere to keep its
window state (nesting depth, owned watchpoint, config epoch). The
lookup runs in kprobe and NMI-like contexts, so it must not allocate
or take locks.
> diff --git a/mm/kwatch/task_ctx.c b/mm/kwatch/task_ctx.c
> new file mode 100644
> index 0000000000000..64383a4429e7c
> --- /dev/null
> +++ b/mm/kwatch/task_ctx.c
[ ... ]
> @@ -0,0 +1,125 @@
> +int kwatch_tsk_ctx_prealloc(u16 max_concurrency)
> +{
> + if (max_concurrency < KWATCH_CTX_POOL_MIN)
> + max_concurrency = KWATCH_CTX_POOL_MIN;
> + else if (max_concurrency > KWATCH_CTX_POOL_MAX)
> + max_concurrency = KWATCH_CTX_POOL_MAX;
> +
> + /*
> + * Set the size/mask only when actually allocating, so they can never
> + * drift out of sync with the live pool if prealloc is ever called
> + * again without a matching free.
> + */
> + if (unlikely(!kwatch_ctx_pool)) {
> + kwatch_ctx_pool_size = roundup_pow_of_two(max_concurrency);
> + kwatch_ctx_pool_mask = kwatch_ctx_pool_size - 1;
> +
> + kwatch_ctx_pool = kcalloc(kwatch_ctx_pool_size,
> + sizeof(struct kwatch_tsk_ctx),
> + GFP_KERNEL);
[Severity: Medium]
Does this allocation risk frequent failure on fragmented systems?
In kwatch_tsk_ctx_prealloc(), this uses kcalloc with GFP_KERNEL to allocate up
to 32768 elements of struct kwatch_tsk_ctx. Such large physically contiguous
allocations are highly prone to failure when memory is fragmented.
Would using kvcalloc be more appropriate here to allow fallback to
non-contiguous virtual memory mappings?
[Severity: High]
Are there missing memory barriers here when publishing kwatch_ctx_pool?
In kwatch_tsk_ctx_prealloc(), kwatch_ctx_pool_size is initialized before
dynamically assigning kwatch_ctx_pool. Without smp_store_release, the CPU
could reorder these stores.
If a concurrent reader in kwatch_tsk_ctx_get() evaluates !kwatch_ctx_pool
without smp_load_acquire, could it observe kwatch_ctx_pool != NULL but read a
stale value of 0 for kwatch_ctx_pool_size? This would result in ilog2(0),
which returns -1, and cause a bitshift overflow when passed to hash_ptr().
> + if (!kwatch_ctx_pool)
> + return -ENOMEM;
> + }
> + return 0;
> +}
> +
> +struct kwatch_tsk_ctx *kwatch_tsk_ctx_get(bool can_alloc)
> +{
> + int start_idx, i, idx;
> + struct task_struct *t;
> +
> + if (unlikely(!kwatch_ctx_pool))
> + return NULL;
> +
> + start_idx = hash_ptr(current, ilog2(kwatch_ctx_pool_size));
> +
> + for (i = 0; i < kwatch_ctx_pool_size; i++) {
> + idx = (start_idx + i) & kwatch_ctx_pool_mask;
[Severity: High]
Does this loop cause significant NMI latency?
In kwatch_tsk_ctx_get(), because the open-addressing hash table nullifies the
task pointer instead of using tombstones for deletions, it cannot stop probing
when encountering an empty slot.
On every cache miss, such as when a task first triggers a watchpoint, this
loop unconditionally iterates kwatch_ctx_pool_size times to ensure the task
is not already present. Is it safe to perform an O(N) scan in NMI context?
> + t = READ_ONCE(kwatch_ctx_pool[idx].task);
> + if (t == current)
> + return &kwatch_ctx_pool[idx];
[Severity: High]
Could this falsely match a new task to a dead task's leaked slot?
In kwatch_tsk_ctx_get(), if a task exits abruptly due to a fatal signal and
bypasses the return probe, it will fail to call kwatch_tsk_ctx_put(), leaving
the slot assigned to the dead task_struct pointer.
When the slab allocator reuses the freed memory for a new task, the new task's
current pointer would match this leaked slot. Will the new task erroneously
inherit stale state and potentially trigger a use-after-free on the dangling
watchpoint?
> + }
> +
> + if (!can_alloc)
> + return NULL;
> +
> + for (i = 0; i < kwatch_ctx_pool_size; i++) {
> + idx = (start_idx + i) & kwatch_ctx_pool_mask;
> + t = READ_ONCE(kwatch_ctx_pool[idx].task);
> + if (!t) {
> + if (!cmpxchg(&kwatch_ctx_pool[idx].task, NULL, current))
> + return &kwatch_ctx_pool[idx];
[Severity: High]
Can an NMI interrupting this path cause a premature release or double free?
In kwatch_tsk_ctx_get(), this lockless claim publishes ownership before the
caller can establish safe nesting state. If an NMI interrupts the task
immediately after this claim, enters a watch window, and reclaims the same
slot because t == current, it will unconditionally call kwatch_tsk_ctx_put()
when exiting the watch window.
This releases the slot by writing NULL to ctx->task. When the interrupted task
resumes, it will incorrectly assume ownership of a now-freed slot that might
be claimed by another CPU. Is there a way to prevent this reentrancy issue?
> + }
> + }
> +
> + return NULL;
> +}
[ ... ]
> +void kwatch_tsk_ctx_free(void)
> +{
> + kfree(kwatch_ctx_pool);
> + kwatch_ctx_pool = NULL;
[Severity: High]
Does this create a use-after-free regression?
In kwatch_tsk_ctx_free(), kfree is called directly on kwatch_ctx_pool followed
by assigning it to NULL, without any RCU synchronization.
If an NMI or task concurrently executes kwatch_tsk_ctx_get(), won't it
dereference the freed memory? Additionally, since kwatch_tsk_ctx_get()
evaluates kwatch_ctx_pool in loops without READ_ONCE, could the compiler
reload the global pointer after it is set to NULL, causing a NULL pointer
dereference?
> +}
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260717125023.1895892-1-wangjinchao600@gmail.com?part=6
^ permalink raw reply [flat|nested] 24+ messages in thread
* [RFC PATCH v2 07/13] stacktrace: export stack_trace_save_regs()
2026-07-17 12:50 [RFC PATCH v2 00/13] mm/kwatch: dynamic hardware watchpoints for hunting memory corruption Jinchao Wang
` (5 preceding siblings ...)
2026-07-17 13:04 ` [RFC PATCH v2 06/13] mm/kwatch: add lockless per-task context pool Jinchao Wang
@ 2026-07-17 13:04 ` Jinchao Wang
2026-07-17 13:05 ` [RFC PATCH v2 08/13] mm/kwatch: add hardware breakpoint backend Jinchao Wang
` (6 subsequent siblings)
13 siblings, 0 replies; 24+ messages in thread
From: Jinchao Wang @ 2026-07-17 13:04 UTC (permalink / raw)
To: Andrew Morton, Peter Zijlstra, Thomas Gleixner, Steven Rostedt,
Masami Hiramatsu
Cc: Ingo Molnar, Borislav Petkov, Dave Hansen, H . Peter Anvin, x86,
Arnaldo Carvalho de Melo, Namhyung Kim, Mark Rutland,
Mathieu Desnoyers, David Hildenbrand, Jonathan Corbet,
Matthew Wilcox, Alan Stern, Randy Dunlap, Alexander Potapenko,
Marco Elver, Mike Rapoport, linux-kernel, linux-mm,
linux-trace-kernel, linux-perf-users, linux-doc, Jinchao Wang
The other stack_trace_save_*() flavours are exported, but the regs
variant is not, so no module can capture a stack trace for a given
pt_regs. KWatch, which may be built as a module, uses it to record
who wrote to a watched address from the hardware breakpoint handler.
Export it like its siblings.
Signed-off-by: Jinchao Wang <wangjinchao600@gmail.com>
---
kernel/stacktrace.c | 2 ++
1 file changed, 2 insertions(+)
diff --git a/kernel/stacktrace.c b/kernel/stacktrace.c
index afb3c116da91..d853c40f916b 100644
--- a/kernel/stacktrace.c
+++ b/kernel/stacktrace.c
@@ -175,6 +175,7 @@ unsigned int stack_trace_save_regs(struct pt_regs *regs, unsigned long *store,
arch_stack_walk(consume_entry, &c, current, regs);
return c.len;
}
+EXPORT_SYMBOL_GPL(stack_trace_save_regs);
#ifdef CONFIG_HAVE_RELIABLE_STACKTRACE
/**
@@ -325,6 +326,7 @@ unsigned int stack_trace_save_regs(struct pt_regs *regs, unsigned long *store,
save_stack_trace_regs(regs, &trace);
return trace.nr_entries;
}
+EXPORT_SYMBOL_GPL(stack_trace_save_regs);
#ifdef CONFIG_HAVE_RELIABLE_STACKTRACE
/**
--
2.53.0
^ permalink raw reply related [flat|nested] 24+ messages in thread* [RFC PATCH v2 08/13] mm/kwatch: add hardware breakpoint backend
2026-07-17 12:50 [RFC PATCH v2 00/13] mm/kwatch: dynamic hardware watchpoints for hunting memory corruption Jinchao Wang
` (6 preceding siblings ...)
2026-07-17 13:04 ` [RFC PATCH v2 07/13] stacktrace: export stack_trace_save_regs() Jinchao Wang
@ 2026-07-17 13:05 ` Jinchao Wang
2026-07-17 13:24 ` sashiko-bot
2026-07-17 13:05 ` [RFC PATCH v2 09/13] mm/kwatch: add probe lifecycle runtime Jinchao Wang
` (5 subsequent siblings)
13 siblings, 1 reply; 24+ messages in thread
From: Jinchao Wang @ 2026-07-17 13:05 UTC (permalink / raw)
To: Andrew Morton, Peter Zijlstra, Thomas Gleixner, Steven Rostedt,
Masami Hiramatsu
Cc: Ingo Molnar, Borislav Petkov, Dave Hansen, H . Peter Anvin, x86,
Arnaldo Carvalho de Melo, Namhyung Kim, Mark Rutland,
Mathieu Desnoyers, David Hildenbrand, Jonathan Corbet,
Matthew Wilcox, Alan Stern, Randy Dunlap, Alexander Potapenko,
Marco Elver, Mike Rapoport, linux-kernel, linux-mm,
linux-trace-kernel, linux-perf-users, linux-doc, Jinchao Wang
Manage a preallocated pool of wide (per-CPU) perf hardware
breakpoints. All breakpoints are registered up front against a dummy
address; arming a watchpoint only re-points an already-registered
event, so the arm path can run from a kprobe handler.
- kwatch_hwbp_get()/put() claim and release pool entries with
per-slot cmpxchg, safe for concurrent consumers on any CPU.
- kwatch_hwbp_arm() updates the local CPU synchronously via
modify_wide_hw_breakpoint_local() and broadcasts asynchronous IPIs
to the other CPUs. Arm-side IPIs are rate-limited per CPU; disarm
IPIs are refcounted so an entry is only recycled once every CPU
has dropped it.
- Hits are reported through the kwatch:kwatch_hit tracepoint with a
short stack trace: the ftrace ring buffer is usable from NMI-like
context and survives a subsequent crash, unlike printk.
- A CPU hotplug callback creates/destroys the per-CPU events as CPUs
come and go.
Signed-off-by: Jinchao Wang <wangjinchao600@gmail.com>
---
include/trace/events/kwatch.h | 68 ++++++
mm/kwatch/Makefile | 2 +-
mm/kwatch/hwbp.c | 388 ++++++++++++++++++++++++++++++++++
3 files changed, 457 insertions(+), 1 deletion(-)
create mode 100644 include/trace/events/kwatch.h
create mode 100644 mm/kwatch/hwbp.c
diff --git a/include/trace/events/kwatch.h b/include/trace/events/kwatch.h
new file mode 100644
index 000000000000..8a2ec6811ad4
--- /dev/null
+++ b/include/trace/events/kwatch.h
@@ -0,0 +1,68 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+#undef TRACE_SYSTEM
+#define TRACE_SYSTEM kwatch
+
+#if !defined(_TRACE_KWATCH_H) || defined(TRACE_HEADER_MULTI_READ)
+#define _TRACE_KWATCH_H
+
+#include <linux/tracepoint.h>
+#include <linux/ptrace.h>
+#include <linux/math64.h>
+
+#define KWATCH_STACK_DEPTH 8
+
+struct trace_seq;
+const char *kwatch_trace_print_stack(struct trace_seq *p,
+ const unsigned long *stack,
+ unsigned int nr);
+
+TRACE_EVENT(kwatch_hit,
+ TP_PROTO(unsigned long ip, unsigned long sp, unsigned long addr,
+ u64 time_ns,
+ unsigned long *stack_entries, unsigned int stack_nr),
+ TP_ARGS(ip, sp, addr, time_ns, stack_entries, stack_nr),
+
+ TP_STRUCT__entry(
+ /*
+ * time_ns first: u64 leading the entry avoids a 4-byte hole
+ * after the unsigned-long fields on 32-bit. stack_nr trails
+ * the fixed fields for the same reason; the stack is a
+ * dynamic array sized to what was actually captured, so a
+ * short trace neither wastes space nor leaks uninitialized
+ * tail slots.
+ */
+ __field(u64, time_ns)
+ __field(unsigned long, ip)
+ __field(unsigned long, sp)
+ __field(unsigned long, addr)
+ __dynamic_array(unsigned long, stack,
+ min_t(unsigned int, stack_nr, KWATCH_STACK_DEPTH))
+ __field(unsigned int, stack_nr)
+ ),
+
+ TP_fast_assign(
+ unsigned long *stack = __get_dynamic_array(stack);
+ unsigned int i;
+
+ __entry->time_ns = time_ns;
+ __entry->ip = ip;
+ __entry->sp = sp;
+ __entry->addr = addr;
+ __entry->stack_nr = min_t(unsigned int, stack_nr,
+ KWATCH_STACK_DEPTH);
+ for (i = 0; i < __entry->stack_nr; i++)
+ stack[i] = stack_entries[i];
+ ),
+
+ TP_printk("KWatch HIT: time=%llu.%06u ip=%pS addr=0x%lx%s",
+ div_u64(__entry->time_ns, 1000000000ULL),
+ (unsigned int)(div_u64(__entry->time_ns, 1000ULL) % 1000000ULL),
+ (void *)__entry->ip, __entry->addr,
+ kwatch_trace_print_stack(p, __get_dynamic_array(stack),
+ __entry->stack_nr))
+);
+
+#endif /* _TRACE_KWATCH_H */
+
+/* This part must be outside protection */
+#include <trace/define_trace.h>
diff --git a/mm/kwatch/Makefile b/mm/kwatch/Makefile
index cc6574df0d68..b2bc3003c89b 100644
--- a/mm/kwatch/Makefile
+++ b/mm/kwatch/Makefile
@@ -1,3 +1,3 @@
obj-$(CONFIG_KWATCH) += kwatch.o
-kwatch-y := deref.o task_ctx.o
+kwatch-y := deref.o task_ctx.o hwbp.o
diff --git a/mm/kwatch/hwbp.c b/mm/kwatch/hwbp.c
new file mode 100644
index 000000000000..d1e93754cce8
--- /dev/null
+++ b/mm/kwatch/hwbp.c
@@ -0,0 +1,388 @@
+// SPDX-License-Identifier: GPL-2.0
+#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
+
+#include <linux/cpuhotplug.h>
+#include <linux/ftrace.h>
+#include <linux/hw_breakpoint.h>
+#include <linux/sched/clock.h>
+#include <linux/irqflags.h>
+#include <linux/kallsyms.h>
+#include <linux/mutex.h>
+#include <linux/printk.h>
+#include <linux/slab.h>
+#include <linux/stacktrace.h>
+#include <linux/trace_seq.h>
+#include <linux/workqueue.h>
+
+#include "kwatch.h"
+
+/* Minimum spacing between cross-CPU arm broadcasts, per CPU. */
+#define KWATCH_ARM_IPI_MIN_INTERVAL_NS 1000000ULL
+
+static LIST_HEAD(kwatch_all_wp_list);
+static struct kwatch_watchpoint **kwatch_wp_slots;
+static u16 kwatch_wp_nr;
+static DEFINE_MUTEX(kwatch_all_wp_mutex);
+static unsigned long kwatch_dummy_holder __aligned(8);
+static int kwatch_hwbp_cpuhp_state = CPUHP_INVALID;
+static atomic_long_t kwatch_arm_ipi_suppressed;
+
+unsigned long kwatch_hwbp_arm_ipi_suppressed(void)
+{
+ return atomic_long_read(&kwatch_arm_ipi_suppressed);
+}
+
+#define CREATE_TRACE_POINTS
+#include <trace/events/kwatch.h>
+
+/*
+ * Render the saved stack like the ftrace built-in stacktrace / dump_stack()
+ * style. Symbol resolution runs at trace read time, not in the hit path.
+ */
+const char *kwatch_trace_print_stack(struct trace_seq *p,
+ const unsigned long *stack,
+ unsigned int nr)
+{
+ const char *ret = trace_seq_buffer_ptr(p);
+ unsigned int i;
+
+ for (i = 0; i < nr; i++)
+ trace_seq_printf(p, "\n => %pS", (void *)stack[i]);
+ trace_seq_putc(p, 0);
+ return ret;
+}
+
+static void kwatch_hwbp_handler(struct perf_event *bp,
+ struct perf_sample_data *data,
+ struct pt_regs *regs)
+{
+ struct kwatch_watchpoint *wp = bp->overflow_handler_context;
+ unsigned long stack_entries[KWATCH_STACK_DEPTH];
+ unsigned int stack_nr;
+
+ if (!kwatch_probe_validate_hit(regs, wp->arm_tsk))
+ return;
+
+ stack_nr = stack_trace_save_regs(regs, stack_entries, KWATCH_STACK_DEPTH, 2);
+ trace_kwatch_hit(instruction_pointer(regs), kernel_stack_pointer(regs),
+ bp->attr.bp_addr, local_clock(),
+ stack_entries, stack_nr);
+}
+
+static void kwatch_hwbp_arm_local(void *info)
+{
+ struct kwatch_watchpoint *wp = info;
+ struct perf_event *bp;
+ unsigned long flags;
+ int cpu, err;
+
+ local_irq_save(flags);
+
+ cpu = smp_processor_id();
+ bp = per_cpu(*wp->event, cpu);
+
+ if (unlikely(!bp))
+ goto out;
+
+ kwatch_probe_mute(true);
+ barrier();
+
+ /*
+ * On success this also updates the per-CPU bp->attr, so the hit
+ * handler reports what THIS CPU is watching instead of the shared
+ * wp->attr, which another CPU may be re-pointing.
+ */
+ err = modify_wide_hw_breakpoint_local(bp, &wp->attr);
+ if (unlikely(err))
+ WARN_ONCE(1,
+ "KWatch: HWBP reinstall failed on CPU%d (err=%d, addr=0x%llx, len=%llu)\n",
+ cpu, err, wp->attr.bp_addr, wp->attr.bp_len);
+
+ barrier();
+ kwatch_probe_mute(false);
+
+out:
+ local_irq_restore(flags);
+}
+
+static inline void kwatch_hwbp_try_recycle(struct kwatch_watchpoint *wp)
+{
+ if (atomic_dec_and_test(&wp->pending_ipis)) {
+ if (!READ_ONCE(wp->teardown))
+ atomic_set_release(&wp->in_use, 0);
+
+ atomic_dec(&wp->refcount);
+ }
+}
+
+static void kwatch_hwbp_disarm_local(void *info)
+{
+ struct kwatch_watchpoint *wp = info;
+
+ kwatch_hwbp_arm_local(info);
+ kwatch_hwbp_try_recycle(wp);
+}
+
+static int kwatch_hwbp_cpu_online(unsigned int cpu)
+{
+ struct perf_event_attr attr;
+ struct kwatch_watchpoint *wp;
+ struct perf_event *bp;
+
+ mutex_lock(&kwatch_all_wp_mutex);
+ list_for_each_entry(wp, &kwatch_all_wp_list, list) {
+ attr = wp->attr;
+ attr.bp_addr = (unsigned long)&kwatch_dummy_holder;
+ bp = perf_event_create_kernel_counter(&attr, cpu, NULL,
+ kwatch_hwbp_handler, wp);
+ if (IS_ERR(bp)) {
+ pr_warn("%s failed to create watch on CPU %d: %ld\n",
+ __func__, cpu, PTR_ERR(bp));
+ continue;
+ }
+ per_cpu(*wp->event, cpu) = bp;
+ }
+ mutex_unlock(&kwatch_all_wp_mutex);
+ return 0;
+}
+
+static int kwatch_hwbp_cpu_offline(unsigned int cpu)
+{
+ struct kwatch_watchpoint *wp;
+ struct perf_event *bp;
+
+ mutex_lock(&kwatch_all_wp_mutex);
+ list_for_each_entry(wp, &kwatch_all_wp_list, list) {
+ bp = per_cpu(*wp->event, cpu);
+ if (bp) {
+ unregister_hw_breakpoint(bp);
+ per_cpu(*wp->event, cpu) = NULL;
+ }
+ }
+ mutex_unlock(&kwatch_all_wp_mutex);
+ return 0;
+}
+
+int kwatch_hwbp_get(struct kwatch_watchpoint **out_wp)
+{
+ struct kwatch_watchpoint *wp;
+ int i;
+
+ /*
+ * Per-slot cmpxchg claim: safe for concurrent consumers on any CPU,
+ * unlike llist_del_first() which requires a single consumer.
+ */
+ for (i = 0; i < kwatch_wp_nr; i++) {
+ wp = kwatch_wp_slots[i];
+ if (atomic_read(&wp->in_use))
+ continue;
+ if (atomic_cmpxchg(&wp->in_use, 0, 1) == 0) {
+ atomic_inc(&wp->refcount);
+ *out_wp = wp;
+ return 0;
+ }
+ }
+ return -EBUSY;
+}
+
+void kwatch_hwbp_arm(struct kwatch_watchpoint *wp, unsigned long addr, u16 len)
+{
+ static DEFINE_PER_CPU(u64, last_ipi_time);
+ int cur_cpu;
+ call_single_data_t *csd;
+ int cpu;
+ bool is_disarm = (addr == (unsigned long)&kwatch_dummy_holder);
+ bool skip_remote = false;
+
+ wp->attr.bp_addr = addr;
+ wp->attr.bp_len = len;
+
+ if (!is_disarm)
+ wp->arm_tsk = current;
+
+ /* ensure attr update visible to other cpu before sending IPI */
+ smp_wmb();
+
+ atomic_set(&wp->pending_ipis, 1);
+ cur_cpu = get_cpu();
+
+ /*
+ * Rate-limit only the cross-CPU broadcast, never the local re-point.
+ * Arming the current CPU is free and must always reflect this window;
+ * only the remote IPI fan-out is throttled to keep a hot function from
+ * storming every CPU. A suppressed broadcast means remote CPUs keep
+ * watching the previous address for that window (a missed remote-CPU
+ * writer is possible) - hence the visible counter, and why kwatch
+ * targets low-frequency functions. Disarm is never throttled: the
+ * slot must always be released.
+ */
+ if (!is_disarm) {
+ u64 now = local_clock();
+ u64 last = this_cpu_read(last_ipi_time);
+
+ if (now - last < KWATCH_ARM_IPI_MIN_INTERVAL_NS) {
+ atomic_long_inc(&kwatch_arm_ipi_suppressed);
+ skip_remote = true;
+ } else {
+ this_cpu_write(last_ipi_time, now);
+ }
+ }
+
+ if (!skip_remote) {
+ for_each_online_cpu(cpu) {
+ if (cpu == cur_cpu)
+ continue;
+
+ if (is_disarm)
+ atomic_inc(&wp->pending_ipis);
+
+ csd = per_cpu_ptr(is_disarm ? wp->csd_disarm : wp->csd_arm,
+ cpu);
+ /*
+ * The arm path ignores a -EBUSY return: a wp has a single
+ * owner (claimed via kwatch_hwbp_get(), held until exit)
+ * and is armed once per window, and the per-CPU csd queue
+ * is FIFO, so this window's csd_arm cannot still be pending
+ * from a prior window (its disarm, queued later, gates the
+ * wp's reuse). Do not "fix" this into a retry.
+ */
+ if (smp_call_function_single_async(cpu, csd) && is_disarm)
+ kwatch_hwbp_try_recycle(wp);
+ }
+ }
+ put_cpu();
+
+ if (is_disarm)
+ kwatch_hwbp_disarm_local(wp);
+ else
+ kwatch_hwbp_arm_local(wp);
+}
+
+int kwatch_hwbp_put(struct kwatch_watchpoint *wp)
+{
+ kwatch_hwbp_arm(wp, (unsigned long)&kwatch_dummy_holder,
+ sizeof(unsigned long));
+
+ return 0;
+}
+
+void kwatch_hwbp_free(void)
+{
+ struct kwatch_watchpoint *wp, *tmp;
+
+ kwatch_wp_nr = 0;
+ kfree(kwatch_wp_slots);
+ kwatch_wp_slots = NULL;
+
+ if (kwatch_hwbp_cpuhp_state != CPUHP_INVALID) {
+ cpuhp_remove_state_nocalls(kwatch_hwbp_cpuhp_state);
+ kwatch_hwbp_cpuhp_state = CPUHP_INVALID;
+ }
+
+ mutex_lock(&kwatch_all_wp_mutex);
+ list_for_each_entry_safe(wp, tmp, &kwatch_all_wp_list, list) {
+ list_del(&wp->list);
+
+ WRITE_ONCE(wp->teardown, true);
+ atomic_dec(&wp->refcount);
+
+ /* Wait for all async IPIs to finish */
+ while (atomic_read(&wp->refcount) > 0)
+ cpu_relax();
+
+ unregister_wide_hw_breakpoint(wp->event);
+ free_percpu(wp->csd_arm);
+ free_percpu(wp->csd_disarm);
+ kfree(wp);
+ }
+ mutex_unlock(&kwatch_all_wp_mutex);
+}
+
+int kwatch_hwbp_prealloc(u16 max_watch)
+{
+ struct kwatch_watchpoint *wp;
+ int success = 0, cpu;
+ int ret;
+
+ atomic_long_set(&kwatch_arm_ipi_suppressed, 0);
+
+ while (!max_watch || success < max_watch) {
+ wp = kzalloc_obj(*wp);
+ if (!wp)
+ break;
+
+ wp->csd_arm = alloc_percpu(call_single_data_t);
+ wp->csd_disarm = alloc_percpu(call_single_data_t);
+ if (!wp->csd_arm || !wp->csd_disarm) {
+ free_percpu(wp->csd_arm);
+ free_percpu(wp->csd_disarm);
+ kfree(wp);
+ break;
+ }
+
+ for_each_possible_cpu(cpu) {
+ INIT_CSD(per_cpu_ptr(wp->csd_arm, cpu),
+ kwatch_hwbp_arm_local, wp);
+ INIT_CSD(per_cpu_ptr(wp->csd_disarm, cpu),
+ kwatch_hwbp_disarm_local, wp);
+ }
+
+ wp->teardown = false;
+
+ hw_breakpoint_init(&wp->attr);
+ wp->attr.bp_addr = (unsigned long)&kwatch_dummy_holder;
+ wp->attr.bp_len = sizeof(unsigned long);
+ /* kwatch localizes corruption: it always watches for writes. */
+ wp->attr.bp_type = HW_BREAKPOINT_W;
+
+ wp->event = register_wide_hw_breakpoint(&wp->attr,
+ kwatch_hwbp_handler,
+ wp);
+ if (IS_ERR_PCPU(wp->event)) {
+ free_percpu(wp->csd_arm);
+ free_percpu(wp->csd_disarm);
+ kfree(wp);
+ break;
+ }
+
+ atomic_set(&wp->refcount, 1);
+
+ mutex_lock(&kwatch_all_wp_mutex);
+ list_add(&wp->list, &kwatch_all_wp_list);
+ mutex_unlock(&kwatch_all_wp_mutex);
+ success++;
+ }
+
+ if (!success)
+ return -EBUSY;
+
+ /*
+ * A fresh prealloc must start from an empty slot array; warn if a
+ * previous session was not torn down, since refilling without a reset
+ * would index past the freshly sized array.
+ */
+ WARN_ON_ONCE(kwatch_wp_slots || kwatch_wp_nr);
+ kwatch_wp_nr = 0;
+
+ kwatch_wp_slots = kcalloc(success, sizeof(*kwatch_wp_slots),
+ GFP_KERNEL);
+ if (!kwatch_wp_slots) {
+ kwatch_hwbp_free();
+ return -ENOMEM;
+ }
+ mutex_lock(&kwatch_all_wp_mutex);
+ list_for_each_entry(wp, &kwatch_all_wp_list, list)
+ kwatch_wp_slots[kwatch_wp_nr++] = wp;
+ mutex_unlock(&kwatch_all_wp_mutex);
+
+ ret = cpuhp_setup_state_nocalls(CPUHP_AP_ONLINE_DYN, "kwatch:online",
+ kwatch_hwbp_cpu_online,
+ kwatch_hwbp_cpu_offline);
+ if (ret < 0) {
+ kwatch_hwbp_free();
+ return ret;
+ }
+
+ kwatch_hwbp_cpuhp_state = ret;
+ return 0;
+}
--
2.53.0
^ permalink raw reply related [flat|nested] 24+ messages in thread* Re: [RFC PATCH v2 08/13] mm/kwatch: add hardware breakpoint backend
2026-07-17 13:05 ` [RFC PATCH v2 08/13] mm/kwatch: add hardware breakpoint backend Jinchao Wang
@ 2026-07-17 13:24 ` sashiko-bot
0 siblings, 0 replies; 24+ messages in thread
From: sashiko-bot @ 2026-07-17 13:24 UTC (permalink / raw)
To: Jinchao Wang; +Cc: linux-perf-users
Thank you for your contribution! Sashiko AI review found 9 potential issue(s) to consider:
- [High] Modifying the trace_seq buffer directly during TP_printk argument evaluation prepends the stack trace and corrupts the trace output.
- [High] Race condition in CPU offline callback leading to Use-After-Free in the IPI handler.
- [High] Concurrent read and write of the shared `wp->attr` structure across CPUs can result in torn reads and invalid hardware breakpoint configuration.
- [High] Untracked csd_arm IPIs can execute after the watchpoint is freed if the target CPU goes offline.
- [High] The `wp->arm_tsk` pointer is assigned to `current` without holding a reference, causing a Use-After-Free if the task exits.
- [High] Missing NULL check on `register_wide_hw_breakpoint` return value causes a kernel panic if hardware breakpoints are unsupported.
- [Medium] Newly onlined CPUs are unconditionally forced to watch the dummy address, missing hits if the watchpoint is currently active.
- [Medium] Per-CPU throttling of arm IPIs causes unrelated watchpoints to be silently ignored on remote CPUs.
- [Medium] A race gap between `register_wide_hw_breakpoint` and `cpuhp_setup_state_nocalls` drops watchpoints on concurrently hotplugged CPUs.
--
--- Patch [8]: [RFC PATCH v2 08/13] mm/kwatch: add hardware breakpoint backend ---
commit 6c7794f69c285985dbf3f0583ef594237690a973
Author: Jinchao Wang <wangjinchao600@gmail.com>
mm/kwatch: add hardware breakpoint backend
Manage a preallocated pool of wide (per-CPU) perf hardware breakpoints.
All breakpoints are registered up front against a dummy address; arming a
watchpoint only re-points an already-registered event, so the arm path can
run from a kprobe handler.
> diff --git a/include/trace/events/kwatch.h b/include/trace/events/kwatch.h
> index 0000000000000..8a2ec6811ad49
> --- /dev/null
> +++ b/include/trace/events/kwatch.h
[ ... ]
> + TP_printk("KWatch HIT: time=%llu.%06u ip=%pS addr=0x%lx%s",
> + div_u64(__entry->time_ns, 1000000000ULL),
> + (unsigned int)(div_u64(__entry->time_ns, 1000ULL) % 1000000ULL),
> + (void *)__entry->ip, __entry->addr,
> + kwatch_trace_print_stack(p, __get_dynamic_array(stack),
> + __entry->stack_nr))
[Severity: High]
Does evaluating kwatch_trace_print_stack() as a TP_printk() argument modify
the trace_seq buffer prematurely?
Because C argument evaluation order is unsequenced, calling trace_seq_printf()
directly inside kwatch_trace_print_stack() mutates the trace_seq buffer p
before or while the main format string is processed.
This causes the output to be written out-of-order or truncated.
> diff --git a/mm/kwatch/hwbp.c b/mm/kwatch/hwbp.c
> index 0000000000000..d1e93754cce82
> --- /dev/null
> +++ b/mm/kwatch/hwbp.c
[ ... ]
> +static void kwatch_hwbp_handler(struct perf_event *bp,
> + struct perf_sample_data *data,
> + struct pt_regs *regs)
> +{
> + struct kwatch_watchpoint *wp = bp->overflow_handler_context;
> + unsigned long stack_entries[KWATCH_STACK_DEPTH];
> + unsigned int stack_nr;
> +
> + if (!kwatch_probe_validate_hit(regs, wp->arm_tsk))
> + return;
[Severity: High]
Does this code dereference a freed task_struct if the arming task exits?
When armed, wp->arm_tsk is assigned to current without taking a reference
count. If the task exits, a subsequent breakpoint hit will dereference a
freed task, leading to a use-after-free and kernel panic.
[ ... ]
> +static void kwatch_hwbp_arm_local(void *info)
> +{
> + struct kwatch_watchpoint *wp = info;
> + struct perf_event *bp;
> + unsigned long flags;
> + int cpu, err;
> +
> + local_irq_save(flags);
> +
> + cpu = smp_processor_id();
> + bp = per_cpu(*wp->event, cpu);
[Severity: High]
Can this dereference a freed perf_event or a NULL pointer?
If a concurrent CPU offline event unregisters and frees the perf_event, the
per-CPU pointer might be read here before it is nullified in
kwatch_hwbp_cpu_offline().
Additionally, if register_wide_hw_breakpoint() returned NULL during
initialization (which IS_ERR_PCPU does not catch), this will attempt to
access the NULL pointer.
> +
> + if (unlikely(!bp))
> + goto out;
> +
> + kwatch_probe_mute(true);
> + barrier();
> +
> + /*
> + * On success this also updates the per-CPU bp->attr, so the hit
> + * handler reports what THIS CPU is watching instead of the shared
> + * wp->attr, which another CPU may be re-pointing.
> + */
> + err = modify_wide_hw_breakpoint_local(bp, &wp->attr);
[Severity: High]
Can this read a torn or partially updated wp->attr structure?
The shared wp->attr structure is modified by kwatch_hwbp_arm() on another
CPU without locking or WRITE_ONCE(). Remote CPUs executing this IPI
concurrently can observe torn reads, leading to an invalid hardware
breakpoint configuration.
[ ... ]
> +static int kwatch_hwbp_cpu_online(unsigned int cpu)
> +{
> + struct perf_event_attr attr;
> + struct kwatch_watchpoint *wp;
> + struct perf_event *bp;
> +
> + mutex_lock(&kwatch_all_wp_mutex);
> + list_for_each_entry(wp, &kwatch_all_wp_list, list) {
> + attr = wp->attr;
> + attr.bp_addr = (unsigned long)&kwatch_dummy_holder;
[Severity: Medium]
Will this unconditional override cause a newly onlined CPU to miss active hits?
If a watchpoint is currently armed and active, bringing a CPU online forces
the new CPU to watch the dummy address instead of the currently watched
address.
[ ... ]
> +static int kwatch_hwbp_cpu_offline(unsigned int cpu)
> +{
> + struct kwatch_watchpoint *wp;
> + struct perf_event *bp;
> +
> + mutex_lock(&kwatch_all_wp_mutex);
> + list_for_each_entry(wp, &kwatch_all_wp_list, list) {
> + bp = per_cpu(*wp->event, cpu);
> + if (bp) {
> + unregister_hw_breakpoint(bp);
> + per_cpu(*wp->event, cpu) = NULL;
[Severity: High]
Does unregistering the breakpoint before setting the pointer to NULL create a
race condition?
If an IPI broadcast from kwatch_hwbp_arm() occurs concurrently, it can read
the stale pointer in kwatch_hwbp_arm_local() and access the freed memory.
> + }
> + }
> + mutex_unlock(&kwatch_all_wp_mutex);
> + return 0;
> +}
[ ... ]
> +void kwatch_hwbp_arm(struct kwatch_watchpoint *wp, unsigned long addr, u16 len)
> +{
> + static DEFINE_PER_CPU(u64, last_ipi_time);
> + int cur_cpu;
> + call_single_data_t *csd;
> + int cpu;
> + bool is_disarm = (addr == (unsigned long)&kwatch_dummy_holder);
> + bool skip_remote = false;
> +
> + wp->attr.bp_addr = addr;
> + wp->attr.bp_len = len;
[Severity: High]
Can mutating these shared fields concurrently cause torn reads?
Because wp->attr is modified without locking or WRITE_ONCE(), remote CPUs
processing the arm IPI via modify_wide_hw_breakpoint_local() might read
partially updated states.
> +
> + if (!is_disarm)
> + wp->arm_tsk = current;
[Severity: High]
Can this result in a use-after-free on current?
No reference count is taken when storing the current task pointer. If the
task exits, a subsequent breakpoint hit will dereference a freed task.
> +
> + /* ensure attr update visible to other cpu before sending IPI */
> + smp_wmb();
> +
> + atomic_set(&wp->pending_ipis, 1);
> + cur_cpu = get_cpu();
> +
> + /*
> + * Rate-limit only the cross-CPU broadcast, never the local re-point.
> + * Arming the current CPU is free and must always reflect this window;
> + * only the remote IPI fan-out is throttled to keep a hot function from
> + * storming every CPU. A suppressed broadcast means remote CPUs keep
> + * watching the previous address for that window (a missed remote-CPU
> + * writer is possible) - hence the visible counter, and why kwatch
> + * targets low-frequency functions. Disarm is never throttled: the
> + * slot must always be released.
> + */
> + if (!is_disarm) {
> + u64 now = local_clock();
> + u64 last = this_cpu_read(last_ipi_time);
> +
> + if (now - last < KWATCH_ARM_IPI_MIN_INTERVAL_NS) {
> + atomic_long_inc(&kwatch_arm_ipi_suppressed);
> + skip_remote = true;
[Severity: Medium]
Will this per-CPU throttling drop arm IPIs for entirely independent watchpoints?
Since last_ipi_time is stored per-CPU rather than per-watchpoint, arming two
distinct watchpoints in rapid succession from the same CPU will cause the
second watchpoint to be silently ignored on remote CPUs.
> + } else {
> + this_cpu_write(last_ipi_time, now);
> + }
> + }
> +
> + if (!skip_remote) {
> + for_each_online_cpu(cpu) {
> + if (cpu == cur_cpu)
> + continue;
> +
> + if (is_disarm)
> + atomic_inc(&wp->pending_ipis);
> +
> + csd = per_cpu_ptr(is_disarm ? wp->csd_disarm : wp->csd_arm,
> + cpu);
[Severity: High]
Can leaving arm IPIs untracked result in a use-after-free?
Because only disarm IPIs are tracked by pending_ipis, an active csd_arm IPI
might still be queued on an offline-going CPU when kwatch_hwbp_free() is
called and frees the csd_arm structure.
[ ... ]
> +void kwatch_hwbp_free(void)
> +{
> + struct kwatch_watchpoint *wp, *tmp;
> +
> + kwatch_wp_nr = 0;
> + kfree(kwatch_wp_slots);
> + kwatch_wp_slots = NULL;
> +
> + if (kwatch_hwbp_cpuhp_state != CPUHP_INVALID) {
> + cpuhp_remove_state_nocalls(kwatch_hwbp_cpuhp_state);
> + kwatch_hwbp_cpuhp_state = CPUHP_INVALID;
> + }
> +
> + mutex_lock(&kwatch_all_wp_mutex);
> + list_for_each_entry_safe(wp, tmp, &kwatch_all_wp_list, list) {
> + list_del(&wp->list);
> +
> + WRITE_ONCE(wp->teardown, true);
> + atomic_dec(&wp->refcount);
> +
> + /* Wait for all async IPIs to finish */
> + while (atomic_read(&wp->refcount) > 0)
> + cpu_relax();
> +
> + unregister_wide_hw_breakpoint(wp->event);
> + free_percpu(wp->csd_arm);
[Severity: High]
Is it safe to free csd_arm here while untracked arm IPIs might still be pending?
Since csd_arm IPIs are not tracked by pending_ipis, a delayed or queued
arm IPI on an offline-going CPU could access the freed csd_arm.
[ ... ]
> +int kwatch_hwbp_prealloc(u16 max_watch)
> +{
> + struct kwatch_watchpoint *wp;
> + int success = 0, cpu;
> + int ret;
[ ... ]
> + wp->event = register_wide_hw_breakpoint(&wp->attr,
> + kwatch_hwbp_handler,
> + wp);
> + if (IS_ERR_PCPU(wp->event)) {
[Severity: High]
Can wp->event be NULL if hardware breakpoints are unsupported?
register_wide_hw_breakpoint() can return NULL. The IS_ERR_PCPU() check does
not catch a NULL return value, causing a kernel panic when
kwatch_hwbp_arm_local() later attempts to dereference the NULL pointer.
[Severity: Medium]
Does registering breakpoints before setting up CPU hotplug callbacks create a
race gap?
If a CPU comes online between this register_wide_hw_breakpoint() loop and the
cpuhp_setup_state_nocalls() below, the new CPU will miss the watchpoint
creation.
[ ... ]
> + ret = cpuhp_setup_state_nocalls(CPUHP_AP_ONLINE_DYN, "kwatch:online",
> + kwatch_hwbp_cpu_online,
> + kwatch_hwbp_cpu_offline);
> + if (ret < 0) {
> + kwatch_hwbp_free();
> + return ret;
> + }
> +
> + kwatch_hwbp_cpuhp_state = ret;
> + return 0;
> +}
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260717125023.1895892-1-wangjinchao600@gmail.com?part=8
^ permalink raw reply [flat|nested] 24+ messages in thread
* [RFC PATCH v2 09/13] mm/kwatch: add probe lifecycle runtime
2026-07-17 12:50 [RFC PATCH v2 00/13] mm/kwatch: dynamic hardware watchpoints for hunting memory corruption Jinchao Wang
` (7 preceding siblings ...)
2026-07-17 13:05 ` [RFC PATCH v2 08/13] mm/kwatch: add hardware breakpoint backend Jinchao Wang
@ 2026-07-17 13:05 ` Jinchao Wang
2026-07-17 13:20 ` sashiko-bot
2026-07-17 13:06 ` [RFC PATCH v2 10/13] mm/kwatch: add anchor thread for global watchpoints Jinchao Wang
` (4 subsequent siblings)
13 siblings, 1 reply; 24+ messages in thread
From: Jinchao Wang @ 2026-07-17 13:05 UTC (permalink / raw)
To: Andrew Morton, Peter Zijlstra, Thomas Gleixner, Steven Rostedt,
Masami Hiramatsu
Cc: Ingo Molnar, Borislav Petkov, Dave Hansen, H . Peter Anvin, x86,
Arnaldo Carvalho de Melo, Namhyung Kim, Mark Rutland,
Mathieu Desnoyers, David Hildenbrand, Jonathan Corbet,
Matthew Wilcox, Alan Stern, Randy Dunlap, Alexander Potapenko,
Marco Elver, Mike Rapoport, linux-kernel, linux-mm,
linux-trace-kernel, linux-perf-users, linux-doc, Jinchao Wang
Open and close the watch window with a kretprobe on the target
function: the entry handler tracks per-task nesting depth and, when
the configured depth is reached, resolves the watch expression and
arms a watchpoint; the exit handler disarms it. An optional kprobe
at func_offset arms mid-function instead of at entry.
Functions running in a real NMI(-like) context are rejected once, at
function entry, by comparing the NMI nesting count against the one
NMI-like layer that int3-based kprobe delivery itself adds; a
companion kprobe with a post_handler pins the probe point so jump
optimization cannot change the delivery mechanism after it is
sampled. Rejections are counted and exposed to the control plane.
A global epoch versioning scheme invalidates stale per-task state
across reconfigurations, and a per-CPU mute flag keeps window
management quiet while a CPU rewrites its own debug registers.
Signed-off-by: Jinchao Wang <wangjinchao600@gmail.com>
---
mm/kwatch/Makefile | 2 +-
mm/kwatch/probe.c | 275 +++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 276 insertions(+), 1 deletion(-)
create mode 100644 mm/kwatch/probe.c
diff --git a/mm/kwatch/Makefile b/mm/kwatch/Makefile
index b2bc3003c89b..f04673cc5b1c 100644
--- a/mm/kwatch/Makefile
+++ b/mm/kwatch/Makefile
@@ -1,3 +1,3 @@
obj-$(CONFIG_KWATCH) += kwatch.o
-kwatch-y := deref.o task_ctx.o hwbp.o
+kwatch-y := deref.o task_ctx.o hwbp.o probe.o
diff --git a/mm/kwatch/probe.c b/mm/kwatch/probe.c
new file mode 100644
index 000000000000..249aa50c9f78
--- /dev/null
+++ b/mm/kwatch/probe.c
@@ -0,0 +1,275 @@
+// SPDX-License-Identifier: GPL-2.0
+#include <linux/atomic.h>
+#include <linux/kprobes.h>
+#include <linux/kallsyms.h>
+#include <linux/percpu.h>
+#include <linux/preempt.h>
+#include <linux/sched.h>
+
+#include "kwatch.h"
+#define TRAMPOLINE_CHECK_DEPTH 16
+static DEFINE_PER_CPU(bool, kwatch_probe_cpu_muted);
+
+struct kwatch_probe_ctx {
+ struct kprobe kp;
+ struct kretprobe rp;
+ struct kprobe pin_kp;
+ const struct kwatch_config *cfg;
+ bool rp_via_int3;
+
+ u32 epoch;
+};
+
+static struct kwatch_probe_ctx kwatch_probe_ctx;
+static atomic_long_t kwatch_nmi_rejected;
+
+unsigned long kwatch_probe_nmi_rejected(void)
+{
+ return atomic_long_read(&kwatch_nmi_rejected);
+}
+
+/*
+ * True if the probed function itself runs in an NMI-like context.
+ * int3-based kprobe delivery adds one NMI-like layer of its own;
+ * delivery is pinned at registration so the subtraction stays exact.
+ */
+static bool kwatch_probed_ctx_in_nmi(bool via_int3)
+{
+ return (preempt_count() & NMI_MASK) > (via_int3 ? NMI_OFFSET : 0);
+}
+
+static void kwatch_pin_post_handler(struct kprobe *p, struct pt_regs *regs,
+ unsigned long flags)
+{
+ /* a post_handler pins the probepoint: no jump optimization */
+}
+
+bool kwatch_probe_validate_hit(struct pt_regs *regs,
+ struct task_struct *arm_tsk)
+{
+ struct kwatch_tsk_ctx *ctx = kwatch_tsk_ctx_get(false);
+ const struct kwatch_config *cfg = kwatch_probe_ctx.cfg;
+
+ if (unlikely(!ctx || !cfg))
+ return true;
+
+ if (arm_tsk != current || ctx->depth != cfg->depth + 1)
+ return true;
+
+ return false;
+}
+
+void kwatch_probe_mute(bool mute)
+{
+ __this_cpu_write(kwatch_probe_cpu_muted, mute);
+}
+
+static inline bool kwatch_probe_is_muted(void)
+{
+ return __this_cpu_read(kwatch_probe_cpu_muted);
+}
+
+enum kwatch_probe_position {
+ KWATCH_PROBE_POSITION_ENTRY,
+ KWATCH_PROBE_POSITION_ACTIVE,
+ KWATCH_PROBE_POSITION_EXIT
+};
+
+static bool kwatch_tsk_ctx_check(enum kwatch_probe_position pos)
+{
+ struct kwatch_tsk_ctx *ctx = kwatch_tsk_ctx_get(true);
+ u32 epoch;
+
+ if (unlikely(!ctx))
+ return false;
+
+ /* Pairs with smp_store_release() in kwatch_probe_start/stop() */
+ epoch = smp_load_acquire(&kwatch_probe_ctx.epoch);
+
+ if (unlikely(ctx->epoch != epoch))
+ kwatch_tsk_ctx_reset(ctx, epoch);
+
+ if (unlikely(!epoch)) {
+ /*
+ * No active session (not yet published, or already stopped):
+ * kwatch_tsk_ctx_get(true) above may have just claimed a slot
+ * for current. Release it here, otherwise an entry that lands
+ * in the register->epoch-publish window leaks the slot until
+ * the pool is freed.
+ */
+ kwatch_tsk_ctx_release(ctx);
+ return false;
+ }
+
+ switch (pos) {
+ case KWATCH_PROBE_POSITION_ENTRY:
+ ctx->depth++;
+ return true;
+ case KWATCH_PROBE_POSITION_ACTIVE:
+ return true;
+ case KWATCH_PROBE_POSITION_EXIT:
+ if (unlikely(ctx->depth == 0)) {
+ kwatch_tsk_ctx_put();
+ return false;
+ }
+
+ ctx->depth--;
+ if (ctx->depth == 0) {
+ kwatch_tsk_ctx_put();
+ return false;
+ }
+ return true;
+ }
+ return false;
+}
+
+static int kwatch_activate_handler(struct kprobe *p, struct pt_regs *regs)
+{
+ struct kwatch_tsk_ctx *ctx = kwatch_tsk_ctx_get(false);
+ unsigned long watch_addr;
+ u16 watch_len;
+
+ if (unlikely(!ctx))
+ return 0;
+
+ if (unlikely(kwatch_probe_is_muted()))
+ return 0;
+
+ if (unlikely(!kwatch_tsk_ctx_check(KWATCH_PROBE_POSITION_ACTIVE)))
+ return 0;
+
+ if (ctx->depth != kwatch_probe_ctx.cfg->depth + 1 || ctx->wp)
+ return 0;
+
+ if (kwatch_deref_resolve(kwatch_probe_ctx.cfg, regs, &watch_addr,
+ &watch_len))
+ return 0;
+
+ if (kwatch_hwbp_get(&ctx->wp))
+ return 0;
+
+ kwatch_hwbp_arm(ctx->wp, watch_addr, watch_len);
+ return 0;
+}
+
+static int kwatch_lifecycle_entry(struct kretprobe_instance *ri,
+ struct pt_regs *regs)
+{
+ /*
+ * Single policy point: the target function's context is judged once
+ * here. A rejected invocation never increments depth, so the offset
+ * kprobe path inherits the verdict through the depth check.
+ */
+ if (unlikely(kwatch_probed_ctx_in_nmi(kwatch_probe_ctx.rp_via_int3))) {
+ atomic_long_inc(&kwatch_nmi_rejected);
+ return 1; /* NMI context is unsupported: no window, no return hook */
+ }
+
+ if (!kwatch_tsk_ctx_check(KWATCH_PROBE_POSITION_ENTRY))
+ return 0;
+
+ if (kwatch_probe_ctx.cfg->func_offset == 0)
+ kwatch_activate_handler(NULL, regs);
+
+ return 0;
+}
+
+static int kwatch_lifecycle_exit(struct kretprobe_instance *ri,
+ struct pt_regs *regs)
+{
+ struct kwatch_tsk_ctx *ctx = kwatch_tsk_ctx_get(false);
+
+ if (unlikely(!ctx))
+ return 0;
+
+ if (!kwatch_tsk_ctx_check(KWATCH_PROBE_POSITION_EXIT))
+ return 0;
+
+ if (ctx->depth == kwatch_probe_ctx.cfg->depth) {
+ struct kwatch_watchpoint *wp = xchg(&ctx->wp, NULL);
+
+ if (wp)
+ kwatch_hwbp_put(wp);
+ }
+
+ return 0;
+}
+
+int kwatch_probe_start(struct kwatch_config *cfg)
+{
+ static u32 next_epoch;
+ u32 current_epoch;
+ int ret;
+
+ /*
+ * Lockless check to prevent concurrent starts. Strictly serialized
+ * by the control plane mutex, but serves as a sanity check.
+ */
+ if (smp_load_acquire(&kwatch_probe_ctx.epoch) != 0)
+ return -EBUSY;
+
+ memset(&kwatch_probe_ctx, 0, sizeof(kwatch_probe_ctx));
+ kwatch_probe_ctx.cfg = cfg;
+
+ /* Session-scoped, like arm_ipi_suppressed in kwatch_hwbp_prealloc() */
+ atomic_long_set(&kwatch_nmi_rejected, 0);
+
+ /*
+ * Pin the entry probepoint before the kretprobe registers, so its
+ * delivery (int3 vs ftrace) can never change under jump optimization.
+ * register_kretprobe() clears kp.post_handler, hence the companion.
+ */
+ kwatch_probe_ctx.pin_kp.symbol_name = cfg->func_name;
+ kwatch_probe_ctx.pin_kp.post_handler = kwatch_pin_post_handler;
+ ret = register_kprobe(&kwatch_probe_ctx.pin_kp);
+ if (ret < 0)
+ return ret;
+
+ kwatch_probe_ctx.rp.entry_handler = kwatch_lifecycle_entry;
+ kwatch_probe_ctx.rp.handler = kwatch_lifecycle_exit;
+ kwatch_probe_ctx.rp.kp.symbol_name = cfg->func_name;
+
+ ret = register_kretprobe(&kwatch_probe_ctx.rp);
+ if (ret < 0) {
+ unregister_kprobe(&kwatch_probe_ctx.pin_kp);
+ return ret;
+ }
+ kwatch_probe_ctx.rp_via_int3 = !kprobe_ftrace(&kwatch_probe_ctx.rp.kp);
+
+ if (cfg->func_offset) {
+ kwatch_probe_ctx.kp.symbol_name = cfg->func_name;
+ kwatch_probe_ctx.kp.offset = cfg->func_offset;
+ kwatch_probe_ctx.kp.pre_handler = kwatch_activate_handler;
+
+ ret = register_kprobe(&kwatch_probe_ctx.kp);
+ if (ret) {
+ unregister_kretprobe(&kwatch_probe_ctx.rp);
+ unregister_kprobe(&kwatch_probe_ctx.pin_kp);
+ return ret;
+ }
+ }
+
+ current_epoch = ++next_epoch;
+ if (unlikely(!current_epoch))
+ current_epoch = ++next_epoch;
+
+ /* Pairs with smp_load_acquire() in kwatch_tsk_ctx_check() */
+ smp_store_release(&kwatch_probe_ctx.epoch, current_epoch);
+
+ return 0;
+}
+
+void kwatch_probe_stop(void)
+{
+ if (!kwatch_probe_ctx.epoch)
+ return;
+
+ /* Pairs with smp_load_acquire() in kwatch_tsk_ctx_check() */
+ smp_store_release(&kwatch_probe_ctx.epoch, 0);
+
+ if (kwatch_probe_ctx.cfg->func_offset > 0)
+ unregister_kprobe(&kwatch_probe_ctx.kp);
+
+ unregister_kretprobe(&kwatch_probe_ctx.rp);
+ unregister_kprobe(&kwatch_probe_ctx.pin_kp);
+}
--
2.53.0
^ permalink raw reply related [flat|nested] 24+ messages in thread* Re: [RFC PATCH v2 09/13] mm/kwatch: add probe lifecycle runtime
2026-07-17 13:05 ` [RFC PATCH v2 09/13] mm/kwatch: add probe lifecycle runtime Jinchao Wang
@ 2026-07-17 13:20 ` sashiko-bot
0 siblings, 0 replies; 24+ messages in thread
From: sashiko-bot @ 2026-07-17 13:20 UTC (permalink / raw)
To: Jinchao Wang; +Cc: linux-perf-users
Thank you for your contribution! Sashiko AI review found 5 potential issue(s) to consider:
- [Critical] Use-After-Free and data race in `kwatch_probe_validate_hit()` during session teardown.
- [High] Mid-function offset kprobes incorrectly inherit NMI context rejection, bypassing safety policies.
- [High] Tasks terminating abnormally leak hardware watchpoints and cause pointer ABA vulnerabilities in the lockless context pool.
- [Medium] Race condition causing false NMI context rejections due to uninitialized delivery flag.
- [Low] Unused macro definition `TRAMPOLINE_CHECK_DEPTH`.
--
--- Patch [9]: [RFC PATCH v2 09/13] mm/kwatch: add probe lifecycle runtime ---
commit 4c4a554ca5428e6d0dbf3c8593ac5647b9e7d585
Author: Jinchao Wang <wangjinchao600@gmail.com>
Subject: mm/kwatch: add probe lifecycle runtime
This commit introduces the probe lifecycle runtime for the watch window using a
kretprobe. It handles function entry and exit, tracks nesting depth to manage
hardware watchpoints, and includes logic for NMI rejection and epoch versioning.
> diff --git a/mm/kwatch/probe.c b/mm/kwatch/probe.c
> new file mode 100644
> index 0000000000000..249aa50c9f782
> --- /dev/null
> +++ b/mm/kwatch/probe.c
> @@ -0,0 +1,275 @@
> +// SPDX-License-Identifier: GPL-2.0
> +#include <linux/atomic.h>
> +#include <linux/kprobes.h>
> +#include <linux/kallsyms.h>
> +#include <linux/percpu.h>
> +#include <linux/preempt.h>
> +#include <linux/sched.h>
> +
> +#include "kwatch.h"
> +#define TRAMPOLINE_CHECK_DEPTH 16
[Severity: Low]
Is this macro used anywhere in the codebase?
[ ... ]
> +static void kwatch_pin_post_handler(struct kprobe *p, struct pt_regs *regs,
> + unsigned long flags)
> +{
> + /* a post_handler pins the probepoint: no jump optimization */
> +}
> +
> +bool kwatch_probe_validate_hit(struct pt_regs *regs,
> + struct task_struct *arm_tsk)
> +{
> + struct kwatch_tsk_ctx *ctx = kwatch_tsk_ctx_get(false);
> + const struct kwatch_config *cfg = kwatch_probe_ctx.cfg;
[Severity: Critical]
If a session is stopped, kwatch_probe_stop() resets the epoch but does not
synchronously disarm hardware watchpoints for tasks still executing inside the
probed function.
Could a leftover watchpoint firing later cause this handler path to locklessly
dereference a freed cfg pointer?
Additionally, if a new session starts concurrently, kwatch_probe_start()
clears the context via memset:
kwatch_probe_start() {
...
memset(&kwatch_probe_ctx, 0, sizeof(kwatch_probe_ctx));
...
}
Might this result in a torn or null pointer dereference here if a hardware
watchpoint fires during the memset?
[ ... ]
> +static int kwatch_activate_handler(struct kprobe *p, struct pt_regs *regs)
> +{
> + struct kwatch_tsk_ctx *ctx = kwatch_tsk_ctx_get(false);
> + unsigned long watch_addr;
> + u16 watch_len;
> +
> + if (unlikely(!ctx))
> + return 0;
> +
> + if (unlikely(kwatch_probe_is_muted()))
> + return 0;
> +
> + if (unlikely(!kwatch_tsk_ctx_check(KWATCH_PROBE_POSITION_ACTIVE)))
> + return 0;
> +
> + if (ctx->depth != kwatch_probe_ctx.cfg->depth + 1 || ctx->wp)
> + return 0;
[Severity: High]
Could an NMI interrupt the probed function after the outer invocation has
already incremented ctx->depth, and then recursively call the same probed
function?
Since kwatch_lifecycle_entry() deliberately skips the ctx->depth increment
for rejected NMIs, would this mid-function offset kprobe falsely pass the
check by reading the outer context's depth and incorrectly arm the hardware
watchpoint using NMI register state?
[ ... ]
> +static int kwatch_lifecycle_entry(struct kretprobe_instance *ri,
> + struct pt_regs *regs)
> +{
> + /*
> + * Single policy point: the target function's context is judged once
> + * here. A rejected invocation never increments depth, so the offset
> + * kprobe path inherits the verdict through the depth check.
> + */
> + if (unlikely(kwatch_probed_ctx_in_nmi(kwatch_probe_ctx.rp_via_int3))) {
> + atomic_long_inc(&kwatch_nmi_rejected);
> + return 1; /* NMI context is unsupported: no window, no return hook */
> + }
[Severity: Medium]
Is there a race window here where rp_via_int3 is read before being
initialized?
kwatch_probe_start() registers the kretprobe, which enables this function to
intercept calls across all CPUs, but it initializes rp_via_int3 slightly
after:
kwatch_probe_start() {
...
ret = register_kretprobe(&kwatch_probe_ctx.rp);
if (ret < 0) {
...
}
kwatch_probe_ctx.rp_via_int3 = !kprobe_ftrace(&kwatch_probe_ctx.rp.kp);
...
}
If another CPU hits this probe before rp_via_int3 is set, might it evaluate
as false and misclassify a normal trap context as an NMI if the architecture
uses int3 for delivery?
[ ... ]
> +static int kwatch_lifecycle_exit(struct kretprobe_instance *ri,
> + struct pt_regs *regs)
> +{
> + struct kwatch_tsk_ctx *ctx = kwatch_tsk_ctx_get(false);
[Severity: High]
If a task is killed by a fatal signal (like SIGKILL) or calls do_exit()
directly while inside the probed function, it bypasses this kretprobe exit
handler.
Does this permanently leak the hardware watchpoint assigned to ctx->wp?
Furthermore, kwatch_tsk_ctx_get() matches raw pointers for the active task:
mm/kwatch/task_ctx.c:kwatch_tsk_ctx_get() {
...
t = READ_ONCE(kwatch_ctx_pool[idx].task);
if (t == current)
return &kwatch_ctx_pool[idx];
...
}
If the slab allocator reuses the dead task's task_struct memory address for a
new task, will the new task erroneously inherit the corrupted state and leaked
watchpoint from the pool?
[ ... ]
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260717125023.1895892-1-wangjinchao600@gmail.com?part=9
^ permalink raw reply [flat|nested] 24+ messages in thread
* [RFC PATCH v2 10/13] mm/kwatch: add anchor thread for global watchpoints
2026-07-17 12:50 [RFC PATCH v2 00/13] mm/kwatch: dynamic hardware watchpoints for hunting memory corruption Jinchao Wang
` (8 preceding siblings ...)
2026-07-17 13:05 ` [RFC PATCH v2 09/13] mm/kwatch: add probe lifecycle runtime Jinchao Wang
@ 2026-07-17 13:06 ` Jinchao Wang
2026-07-17 13:20 ` sashiko-bot
2026-07-17 13:06 ` [RFC PATCH v2 11/13] mm/kwatch: add debugfs control plane Jinchao Wang
` (3 subsequent siblings)
13 siblings, 1 reply; 24+ messages in thread
From: Jinchao Wang @ 2026-07-17 13:06 UTC (permalink / raw)
To: Andrew Morton, Peter Zijlstra, Thomas Gleixner, Steven Rostedt,
Masami Hiramatsu
Cc: Ingo Molnar, Borislav Petkov, Dave Hansen, H . Peter Anvin, x86,
Arnaldo Carvalho de Melo, Namhyung Kim, Mark Rutland,
Mathieu Desnoyers, David Hildenbrand, Jonathan Corbet,
Matthew Wilcox, Alan Stern, Randy Dunlap, Alexander Potapenko,
Marco Elver, Mike Rapoport, linux-kernel, linux-mm,
linux-trace-kernel, linux-perf-users, linux-doc, Jinchao Wang
Global variables have no function whose execution can bound the
watch window. Provide one: a kernel thread sleeps for the configured
duration inside a dedicated noinline function,
kwatch_global_anchor(), and the probe runtime hooks that function
like any other target.
When the duration expires the thread schedules a work item that
tears the session down; the expired flag is cleared under the
control-plane mutex so a stale work item from a previous session
cannot stop a new one.
Signed-off-by: Jinchao Wang <wangjinchao600@gmail.com>
---
mm/kwatch/Makefile | 2 +-
mm/kwatch/anchor.c | 85 ++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 86 insertions(+), 1 deletion(-)
create mode 100644 mm/kwatch/anchor.c
diff --git a/mm/kwatch/Makefile b/mm/kwatch/Makefile
index f04673cc5b1c..b196c794619a 100644
--- a/mm/kwatch/Makefile
+++ b/mm/kwatch/Makefile
@@ -1,3 +1,3 @@
obj-$(CONFIG_KWATCH) += kwatch.o
-kwatch-y := deref.o task_ctx.o hwbp.o probe.o
+kwatch-y := deref.o task_ctx.o hwbp.o probe.o anchor.o
diff --git a/mm/kwatch/anchor.c b/mm/kwatch/anchor.c
new file mode 100644
index 000000000000..e87eb5e813ff
--- /dev/null
+++ b/mm/kwatch/anchor.c
@@ -0,0 +1,85 @@
+// SPDX-License-Identifier: GPL-2.0
+#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
+
+#include <linux/kthread.h>
+#include <linux/wait.h>
+#include <linux/jiffies.h>
+#include <linux/err.h>
+#include <linux/workqueue.h>
+
+#include "kwatch.h"
+
+static DECLARE_WAIT_QUEUE_HEAD(kwatch_anchor_wq);
+static struct task_struct *kwatch_anchor_tsk;
+static bool kwatch_anchor_expired;
+
+bool kwatch_anchor_has_expired(void)
+{
+ return READ_ONCE(kwatch_anchor_expired);
+}
+
+void kwatch_anchor_clear_expired(void)
+{
+ WRITE_ONCE(kwatch_anchor_expired, false);
+}
+
+static void kwatch_auto_stop_handler(struct work_struct *work)
+{
+ kwatch_auto_stop();
+}
+
+static DECLARE_WORK(kwatch_auto_stop_work, kwatch_auto_stop_handler);
+
+noinline void kwatch_global_anchor(unsigned long duration_sec)
+{
+ /* TASK_IDLE: a long timed sleep must not inflate loadavg or trip the
+ * hung-task detector the way TASK_UNINTERRUPTIBLE would.
+ */
+ wait_event_idle_timeout(kwatch_anchor_wq, kthread_should_stop(),
+ duration_sec * HZ);
+}
+
+static int kwatch_anchor_thread_fn(void *data)
+{
+ unsigned long duration = (unsigned long)data;
+
+ kwatch_global_anchor(duration);
+
+ if (!kthread_should_stop()) {
+ /* mark before scheduling; cleared under the control mutex */
+ WRITE_ONCE(kwatch_anchor_expired, true);
+ schedule_work(&kwatch_auto_stop_work);
+ }
+
+ while (!kthread_should_stop())
+ schedule_timeout_idle(HZ);
+
+ return 0;
+}
+
+int kwatch_anchor_start(u16 duration)
+{
+ kwatch_anchor_tsk = kthread_run(kwatch_anchor_thread_fn,
+ (void *)(unsigned long)duration,
+ "kwatch_anchor");
+ if (IS_ERR(kwatch_anchor_tsk)) {
+ int ret = PTR_ERR(kwatch_anchor_tsk);
+
+ kwatch_anchor_tsk = NULL;
+ return ret;
+ }
+ return 0;
+}
+
+void kwatch_anchor_stop(void)
+{
+ if (kwatch_anchor_tsk) {
+ kthread_stop(kwatch_anchor_tsk);
+ kwatch_anchor_tsk = NULL;
+ }
+}
+
+void kwatch_anchor_cancel_work(void)
+{
+ cancel_work_sync(&kwatch_auto_stop_work);
+}
--
2.53.0
^ permalink raw reply related [flat|nested] 24+ messages in thread* Re: [RFC PATCH v2 10/13] mm/kwatch: add anchor thread for global watchpoints
2026-07-17 13:06 ` [RFC PATCH v2 10/13] mm/kwatch: add anchor thread for global watchpoints Jinchao Wang
@ 2026-07-17 13:20 ` sashiko-bot
0 siblings, 0 replies; 24+ messages in thread
From: sashiko-bot @ 2026-07-17 13:20 UTC (permalink / raw)
To: Jinchao Wang; +Cc: linux-perf-users
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [Medium] `kwatch_anchor_start()` overwrites `kwatch_anchor_tsk` without checking if a thread is already running, permanently leaking the previous kernel thread.
- [Low] Undefined reference to `kwatch_auto_stop` causing a linker error.
--
--- Patch [10]: [RFC PATCH v2 10/13] mm/kwatch: add anchor thread for global watchpoints ---
commit a3eb869f890aaa4760c373cf20d5f98126ae7d23
Author: Jinchao Wang <wangjinchao600@gmail.com>
mm/kwatch: add anchor thread for global watchpoints
This patch adds a kernel thread that sleeps for a configured duration to
bound the watch window for global variables. When the duration expires,
the thread schedules a work item to tear down the session.
> diff --git a/mm/kwatch/anchor.c b/mm/kwatch/anchor.c
> new file mode 100644
> index 0000000000000..e87eb5e813ff2
> --- /dev/null
> +++ b/mm/kwatch/anchor.c
[ ... ]
> +static void kwatch_auto_stop_handler(struct work_struct *work)
> +{
> + kwatch_auto_stop();
> +}
[Severity: Low]
Does this cause a linker error? kwatch_auto_stop() is called here, but it
appears to be undefined in the tree, which would result in a build failure
when CONFIG_KWATCH=y is enabled.
[ ... ]
> +int kwatch_anchor_start(u16 duration)
> +{
> + kwatch_anchor_tsk = kthread_run(kwatch_anchor_thread_fn,
> + (void *)(unsigned long)duration,
> + "kwatch_anchor");
> + if (IS_ERR(kwatch_anchor_tsk)) {
[Severity: Medium]
What happens if this function is called while an anchor thread is already
running? The global kwatch_anchor_tsk pointer is unconditionally overwritten
here without verifying if a previous thread is active or stopping it.
Since the leaked thread loops waiting for kthread_should_stop() on a lost
task pointer, could this permanently leak the previous thread's task_struct
and stack?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260717125023.1895892-1-wangjinchao600@gmail.com?part=10
^ permalink raw reply [flat|nested] 24+ messages in thread
* [RFC PATCH v2 11/13] mm/kwatch: add debugfs control plane
2026-07-17 12:50 [RFC PATCH v2 00/13] mm/kwatch: dynamic hardware watchpoints for hunting memory corruption Jinchao Wang
` (9 preceding siblings ...)
2026-07-17 13:06 ` [RFC PATCH v2 10/13] mm/kwatch: add anchor thread for global watchpoints Jinchao Wang
@ 2026-07-17 13:06 ` Jinchao Wang
2026-07-17 13:27 ` sashiko-bot
2026-07-17 13:07 ` [RFC PATCH v2 12/13] mm/kwatch: add KUnit tests for the watch expression parser Jinchao Wang
` (2 subsequent siblings)
13 siblings, 1 reply; 24+ messages in thread
From: Jinchao Wang @ 2026-07-17 13:06 UTC (permalink / raw)
To: Andrew Morton, Peter Zijlstra, Thomas Gleixner, Steven Rostedt,
Masami Hiramatsu
Cc: Ingo Molnar, Borislav Petkov, Dave Hansen, H . Peter Anvin, x86,
Arnaldo Carvalho de Melo, Namhyung Kim, Mark Rutland,
Mathieu Desnoyers, David Hildenbrand, Jonathan Corbet,
Matthew Wilcox, Alan Stern, Randy Dunlap, Alexander Potapenko,
Marco Elver, Mike Rapoport, linux-kernel, linux-mm,
linux-trace-kernel, linux-perf-users, linux-doc, Jinchao Wang
Wire the pieces together behind a single debugfs file,
/sys/kernel/debug/kwatch/config. Writing a key=value configuration
string stops any active session and starts a new one; reading shows
the active configuration and the nmi_rejected counter. An open-count
guard keeps the file single-open and a mutex serializes
start/stop/auto-stop against each other.
Add the Kconfig entry and hook mm/kwatch into the mm build. KWatch
can be built in or as a module; symbol-name watch expressions need
the built-in flavour (kallsyms_lookup_name is not exported).
Signed-off-by: Jinchao Wang <wangjinchao600@gmail.com>
---
MAINTAINERS | 8 ++
mm/Kconfig | 1 +
mm/Makefile | 1 +
mm/kwatch/Kconfig | 16 +++
mm/kwatch/Makefile | 2 +-
mm/kwatch/core.c | 324 +++++++++++++++++++++++++++++++++++++++++++++
6 files changed, 351 insertions(+), 1 deletion(-)
create mode 100644 mm/kwatch/Kconfig
create mode 100644 mm/kwatch/core.c
diff --git a/MAINTAINERS b/MAINTAINERS
index 7cc4bca5a2c5..b6371f92fe5c 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -14578,6 +14578,14 @@ S: Supported
T: git git://git.kernel.org/pub/scm/virt/kvm/kvm.git
F: arch/x86/kvm/xen.*
+KWATCH
+M: Jinchao Wang <wangjinchao600@gmail.com>
+L: linux-mm@kvack.org
+S: Maintained
+F: Documentation/dev-tools/kwatch.rst
+F: include/trace/events/kwatch.h
+F: mm/kwatch/
+
L3MDEV
M: David Ahern <dsahern@kernel.org>
L: netdev@vger.kernel.org
diff --git a/mm/Kconfig b/mm/Kconfig
index 9e0ca4824905..cac75a46e21a 100644
--- a/mm/Kconfig
+++ b/mm/Kconfig
@@ -1510,5 +1510,6 @@ config LAZY_MMU_MODE_KUNIT_TEST
If unsure, say N.
source "mm/damon/Kconfig"
+source "mm/kwatch/Kconfig"
endmenu
diff --git a/mm/Makefile b/mm/Makefile
index eff9f9e7e061..80c688330358 100644
--- a/mm/Makefile
+++ b/mm/Makefile
@@ -92,6 +92,7 @@ obj-$(CONFIG_PAGE_POISONING) += page_poison.o
obj-$(CONFIG_KASAN) += kasan/
obj-$(CONFIG_KFENCE) += kfence/
obj-$(CONFIG_KMSAN) += kmsan/
+obj-$(CONFIG_KWATCH) += kwatch/
obj-$(CONFIG_FAILSLAB) += failslab.o
obj-$(CONFIG_FAIL_PAGE_ALLOC) += fail_page_alloc.o
obj-$(CONFIG_MEMTEST) += memtest.o
diff --git a/mm/kwatch/Kconfig b/mm/kwatch/Kconfig
new file mode 100644
index 000000000000..9daf6d4463ef
--- /dev/null
+++ b/mm/kwatch/Kconfig
@@ -0,0 +1,16 @@
+config KWATCH
+ tristate "Kernel Watch Framework"
+ depends on PERF_EVENTS && HAVE_HW_BREAKPOINT && DEBUG_FS
+ depends on HAVE_REINSTALL_HW_BREAKPOINT
+ depends on KPROBES && KRETPROBES
+ depends on STACKTRACE
+ help
+ A generalized hardware-assisted memory monitor utility.
+ It provides a low-overhead, real-time trigger mechanism to monitor
+ kernel memory safely in atomic contexts using hardware breakpoints.
+
+ KWatch is designed to catch silent memory corruptions, stack
+ overwrites, and complex Heisenbugs by synchronously trapping the
+ exact instruction causing the illegal access.
+
+ If unsure, say N.
diff --git a/mm/kwatch/Makefile b/mm/kwatch/Makefile
index b196c794619a..02d7917602f1 100644
--- a/mm/kwatch/Makefile
+++ b/mm/kwatch/Makefile
@@ -1,3 +1,3 @@
obj-$(CONFIG_KWATCH) += kwatch.o
-kwatch-y := deref.o task_ctx.o hwbp.o probe.o anchor.o
+kwatch-y := core.o deref.o task_ctx.o hwbp.o probe.o anchor.o
diff --git a/mm/kwatch/core.c b/mm/kwatch/core.c
new file mode 100644
index 000000000000..d8526d5aae5c
--- /dev/null
+++ b/mm/kwatch/core.c
@@ -0,0 +1,324 @@
+// SPDX-License-Identifier: GPL-2.0
+#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
+
+#include <linux/kstrtox.h>
+#include <linux/module.h>
+#include <linux/slab.h>
+#include <linux/string.h>
+#include <linux/types.h>
+#include <linux/atomic.h>
+#include <linux/debugfs.h>
+#include <linux/mutex.h>
+#include "kwatch.h"
+
+static struct kwatch_config kwatch_config;
+static bool watching_active;
+
+static struct dentry *dbgfs_dir;
+static struct dentry *dbgfs_config;
+static DEFINE_MUTEX(kwatch_dbgfs_mutex);
+static atomic_t dbgfs_config_busy = ATOMIC_INIT(0);
+
+static int kwatch_start_watching(void)
+{
+ int ret;
+
+ if (!strlen(kwatch_config.func_name)) {
+ if (kwatch_config.duration > 0) {
+ strscpy(kwatch_config.func_name, "kwatch_global_anchor",
+ sizeof(kwatch_config.func_name));
+ } else {
+ pr_err("func_name or duration is required\n");
+ return -EINVAL;
+ }
+ } else if (kwatch_config.duration > 0 &&
+ strcmp(kwatch_config.func_name, "kwatch_global_anchor")) {
+ pr_warn("duration is ignored when watching a specific function\n");
+ }
+
+ ret = kwatch_hwbp_prealloc(kwatch_config.max_watch);
+ if (ret) {
+ pr_err("kwatch_hwbp_prealloc ret: %d\n", ret);
+ return ret;
+ }
+
+ ret = kwatch_tsk_ctx_prealloc(kwatch_config.max_concurrency);
+ if (ret) {
+ kwatch_hwbp_free();
+ return ret;
+ }
+
+ ret = kwatch_probe_start(&kwatch_config);
+ if (ret) {
+ pr_err("kwatch_probe_start ret: %d\n", ret);
+ kwatch_tsk_ctx_free();
+ kwatch_hwbp_free();
+ return ret;
+ }
+
+ if (!strcmp(kwatch_config.func_name, "kwatch_global_anchor")) {
+ ret = kwatch_anchor_start(kwatch_config.duration);
+ if (ret) {
+ kwatch_probe_stop();
+ synchronize_rcu();
+ kwatch_tsk_ctx_release_wps();
+ kwatch_hwbp_free();
+ kwatch_tsk_ctx_free();
+ return ret;
+ }
+ }
+
+ watching_active = true;
+ return 0;
+}
+
+static void kwatch_stop_watching(void)
+{
+ watching_active = false;
+
+ kwatch_anchor_stop();
+ /* after kthread_stop: the dead thread cannot re-mark expiry */
+ kwatch_anchor_clear_expired();
+
+ kwatch_probe_stop();
+ synchronize_rcu();
+ kwatch_tsk_ctx_release_wps();
+ /*
+ * Waits for disarm IPIs and unregisters breakpoints: no #DB can
+ * reach the ctx pool once this returns.
+ */
+ kwatch_hwbp_free();
+ kwatch_tsk_ctx_free();
+}
+
+void kwatch_auto_stop(void)
+{
+ mutex_lock(&kwatch_dbgfs_mutex);
+ /* the expired check neutralizes work items from torn-down sessions */
+ if (watching_active && kwatch_anchor_has_expired()) {
+ kwatch_stop_watching();
+ pr_info("watch duration expired, stopped watching\n");
+ }
+ mutex_unlock(&kwatch_dbgfs_mutex);
+}
+
+static int kwatch_config_parse(char *buf, struct kwatch_config *cfg)
+{
+ char *token, *key, *val;
+ int ret = 0;
+
+ memset(cfg, 0, sizeof(*cfg));
+ cfg->max_concurrency = 256;
+ cfg->max_watch = 4;
+ cfg->watch_len = 8;
+
+ while ((token = strsep(&buf, " \t\n")) != NULL) {
+ if (!*token)
+ continue;
+ key = strsep(&token, "=");
+ val = token;
+ if (!key || !val)
+ return -EINVAL;
+
+ if (!strcmp(key, "func_name")) {
+ strscpy(cfg->func_name, val, sizeof(cfg->func_name));
+ } else if (!strcmp(key, "func_offset")) {
+ ret = kstrtou16(val, 0, &cfg->func_offset);
+ } else if (!strcmp(key, "depth")) {
+ ret = kstrtou16(val, 0, &cfg->depth);
+ } else if (!strcmp(key, "max_concurrency")) {
+ ret = kstrtou16(val, 0, &cfg->max_concurrency);
+ } else if (!strcmp(key, "max_watch")) {
+ ret = kstrtou16(val, 0, &cfg->max_watch);
+ } else if (!strcmp(key, "watch_len")) {
+ ret = kstrtou16(val, 0, &cfg->watch_len);
+ if (!ret && cfg->watch_len != 1 &&
+ cfg->watch_len != 2 && cfg->watch_len != 4 &&
+ cfg->watch_len != 8)
+ ret = -EINVAL;
+ } else if (!strcmp(key, "duration")) {
+ ret = kstrtou16(val, 0, &cfg->duration);
+ } else if (!strcmp(key, "watch_expr")) {
+ strscpy(cfg->watch_expr, val, sizeof(cfg->watch_expr));
+ ret = kwatch_deref_parse(cfg, val);
+ }
+
+ if (ret)
+ return ret;
+ }
+ return 0;
+}
+
+static int kwatch_dbgfs_open(struct inode *inode, struct file *file)
+{
+ if (atomic_cmpxchg(&dbgfs_config_busy, 0, 1))
+ return -EBUSY;
+ return 0;
+}
+
+static int kwatch_dbgfs_release(struct inode *inode, struct file *file)
+{
+ atomic_set(&dbgfs_config_busy, 0);
+ return 0;
+}
+
+static ssize_t kwatch_dbgfs_read(struct file *file, char __user *user_buf,
+ size_t count, loff_t *ppos)
+{
+ char *out_buf;
+ size_t len = 0;
+ ssize_t ret;
+
+ out_buf = kzalloc(MAX_CONFIG_STR_LEN, GFP_KERNEL);
+ if (!out_buf)
+ return -ENOMEM;
+
+ /*
+ * Serialize against the write path and the auto-stop work item so the
+ * config snapshot cannot tear or race a session teardown.
+ */
+ mutex_lock(&kwatch_dbgfs_mutex);
+
+ if (watching_active) {
+ len += scnprintf(out_buf + len, MAX_CONFIG_STR_LEN - len,
+ "func_name=%s\n"
+ "func_offset=%u\n"
+ "depth=%u\n"
+ "duration=%u\n"
+ "max_concurrency=%u\n"
+ "max_watch=%u\n"
+ "watch_len=%u\n",
+ kwatch_config.func_name,
+ kwatch_config.func_offset, kwatch_config.depth,
+ kwatch_config.duration,
+ kwatch_config.max_concurrency,
+ kwatch_config.max_watch,
+ kwatch_config.watch_len);
+
+ if (kwatch_config.base == KWATCH_BASE_GLOBAL_SYM) {
+ len += scnprintf(out_buf + len, MAX_CONFIG_STR_LEN - len,
+ "sym_addr=0x%lx\n", kwatch_config.sym_addr);
+ }
+
+ len += scnprintf(out_buf + len, MAX_CONFIG_STR_LEN - len,
+ "watch_expr=%s\n"
+ "nmi_rejected=%lu\n"
+ "arm_ipi_suppressed=%lu\n",
+ kwatch_config.watch_expr,
+ kwatch_probe_nmi_rejected(),
+ kwatch_hwbp_arm_ipi_suppressed());
+ } else {
+ len = scnprintf(out_buf, MAX_CONFIG_STR_LEN, "not watching\n");
+ }
+
+ mutex_unlock(&kwatch_dbgfs_mutex);
+
+ ret = simple_read_from_buffer(user_buf, count, ppos, out_buf, len);
+ kfree(out_buf);
+ return ret;
+}
+
+static ssize_t kwatch_dbgfs_write(struct file *file, const char __user *buffer,
+ size_t count, loff_t *ppos)
+{
+ char *input_alloc;
+ char *parse_str;
+ int ret;
+
+ if (count == 0 || count >= MAX_CONFIG_STR_LEN)
+ return -EINVAL;
+
+ input_alloc = memdup_user_nul(buffer, count);
+ if (IS_ERR(input_alloc))
+ return PTR_ERR(input_alloc);
+
+ mutex_lock(&kwatch_dbgfs_mutex);
+
+ if (watching_active)
+ kwatch_stop_watching();
+
+ parse_str = strim(input_alloc);
+
+ if (!strlen(parse_str)) {
+ ret = -EINVAL;
+ goto out;
+ }
+
+ ret = kwatch_config_parse(parse_str, &kwatch_config);
+ if (ret) {
+ pr_err("Failed to parse config %d\n", ret);
+ goto out;
+ }
+
+ ret = kwatch_start_watching();
+ if (ret) {
+ pr_err("Failed to start watching with %d\n", ret);
+ goto out;
+ }
+
+ ret = count;
+
+out:
+ mutex_unlock(&kwatch_dbgfs_mutex);
+ kfree(input_alloc);
+ return ret;
+}
+
+static const struct file_operations kwatch_fops = {
+ .owner = THIS_MODULE,
+ .open = kwatch_dbgfs_open,
+ .release = kwatch_dbgfs_release,
+ .read = kwatch_dbgfs_read,
+ .write = kwatch_dbgfs_write,
+};
+
+static int __init kwatch_init(void)
+{
+ int ret = 0;
+
+ memset(&kwatch_config, 0, sizeof(kwatch_config));
+
+ dbgfs_dir = debugfs_create_dir("kwatch", NULL);
+ if (IS_ERR(dbgfs_dir)) {
+ ret = PTR_ERR(dbgfs_dir);
+ goto err_dir;
+ }
+
+ dbgfs_config = debugfs_create_file("config", 0600, dbgfs_dir, NULL,
+ &kwatch_fops);
+ if (IS_ERR(dbgfs_config)) {
+ ret = PTR_ERR(dbgfs_config);
+ goto err_file;
+ }
+
+ pr_info("module loaded\n");
+ return 0;
+
+err_file:
+ debugfs_remove_recursive(dbgfs_dir);
+ dbgfs_dir = NULL;
+err_dir:
+ return ret;
+}
+module_init(kwatch_init);
+
+static void __exit kwatch_exit(void)
+{
+ mutex_lock(&kwatch_dbgfs_mutex);
+ if (watching_active)
+ kwatch_stop_watching();
+ mutex_unlock(&kwatch_dbgfs_mutex);
+
+ /* the anchor thread is dead: nothing can schedule new work now */
+ kwatch_anchor_cancel_work();
+
+ debugfs_remove_recursive(dbgfs_dir);
+ dbgfs_dir = NULL;
+
+ pr_info("kwatch unloaded\n");
+}
+module_exit(kwatch_exit);
+
+MODULE_AUTHOR("Jinchao Wang <wangjinchao600@gmail.com>");
+MODULE_DESCRIPTION("Kernel watchpoint");
+MODULE_LICENSE("GPL");
--
2.53.0
^ permalink raw reply related [flat|nested] 24+ messages in thread* Re: [RFC PATCH v2 11/13] mm/kwatch: add debugfs control plane
2026-07-17 13:06 ` [RFC PATCH v2 11/13] mm/kwatch: add debugfs control plane Jinchao Wang
@ 2026-07-17 13:27 ` sashiko-bot
0 siblings, 0 replies; 24+ messages in thread
From: sashiko-bot @ 2026-07-17 13:27 UTC (permalink / raw)
To: Jinchao Wang; +Cc: linux-perf-users
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Critical] Use-After-Free in kwatch context pool due to delayed kretprobe exit handlers
--
--- Patch [11]: [RFC PATCH v2 11/13] mm/kwatch: add debugfs control plane ---
commit 7abbeb0e0111172be94e0f025440e5d06358c7a1
Author: Jinchao Wang <wangjinchao600@gmail.com>
mm/kwatch: add debugfs control plane
Wire the pieces together behind a single debugfs file.
Add the Kconfig entry and hook mm/kwatch into the mm build.
> diff --git a/mm/kwatch/core.c b/mm/kwatch/core.c
> new file mode 100644
> index 0000000000000..d8526d5aae5cd
> --- /dev/null
> +++ b/mm/kwatch/core.c
[ ... ]
> @@ -0,0 +1,324 @@
> +static void kwatch_stop_watching(void)
> +{
> + watching_active = false;
> +
> + kwatch_anchor_stop();
> + /* after kthread_stop: the dead thread cannot re-mark expiry */
> + kwatch_anchor_clear_expired();
> +
> + kwatch_probe_stop();
> + synchronize_rcu();
> + kwatch_tsk_ctx_release_wps();
> + /*
> + * Waits for disarm IPIs and unregisters breakpoints: no #DB can
> + * reach the ctx pool once this returns.
> + */
> + kwatch_hwbp_free();
> + kwatch_tsk_ctx_free();
[Severity: Critical]
Is it possible for a sleeping task to access the context pool after it gets
freed here?
If a task enters a watched function that can sleep, it triggers the kretprobe
entry handler and goes to sleep. When the user stops watching, this code
unregisters the probe and calls synchronize_rcu(). Since the sleeping task is
not in an RCU read-side critical section, synchronize_rcu() can return without
waiting for the task to wake up.
The kwatch_ctx_pool is then freed. When the sleeping task eventually wakes up
and returns from the watched function, its kretprobe exit trampoline executes
kwatch_lifecycle_exit(). That exit handler calls kwatch_tsk_ctx_get(), which
dereferences the freed pool, resulting in a use-after-free regression.
> +}
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260717125023.1895892-1-wangjinchao600@gmail.com?part=11
^ permalink raw reply [flat|nested] 24+ messages in thread
* [RFC PATCH v2 12/13] mm/kwatch: add KUnit tests for the watch expression parser
2026-07-17 12:50 [RFC PATCH v2 00/13] mm/kwatch: dynamic hardware watchpoints for hunting memory corruption Jinchao Wang
` (10 preceding siblings ...)
2026-07-17 13:06 ` [RFC PATCH v2 11/13] mm/kwatch: add debugfs control plane Jinchao Wang
@ 2026-07-17 13:07 ` Jinchao Wang
2026-07-17 13:07 ` [RFC PATCH v2 13/13] Documentation/dev-tools: document KWatch Jinchao Wang
2026-07-17 13:41 ` [RFC PATCH v2 00/13] mm/kwatch: dynamic hardware watchpoints for hunting memory corruption Dave Hansen
13 siblings, 0 replies; 24+ messages in thread
From: Jinchao Wang @ 2026-07-17 13:07 UTC (permalink / raw)
To: Andrew Morton, Peter Zijlstra, Thomas Gleixner, Steven Rostedt,
Masami Hiramatsu
Cc: Ingo Molnar, Borislav Petkov, Dave Hansen, H . Peter Anvin, x86,
Arnaldo Carvalho de Melo, Namhyung Kim, Mark Rutland,
Mathieu Desnoyers, David Hildenbrand, Jonathan Corbet,
Matthew Wilcox, Alan Stern, Randy Dunlap, Alexander Potapenko,
Marco Elver, Mike Rapoport, linux-kernel, linux-mm,
linux-trace-kernel, linux-perf-users, linux-doc, Jinchao Wang
Cover base anchors (stack, argN, absolute address), positive and
negative offsets, dereference chains, and rejection of malformed
expressions (missing offsets, bad argument index, junk offsets).
Signed-off-by: Jinchao Wang <wangjinchao600@gmail.com>
---
mm/kwatch/.kunitconfig | 9 +++
mm/kwatch/Kconfig | 12 ++++
mm/kwatch/Makefile | 1 +
mm/kwatch/deref_test.c | 146 +++++++++++++++++++++++++++++++++++++++++
4 files changed, 168 insertions(+)
create mode 100644 mm/kwatch/.kunitconfig
create mode 100644 mm/kwatch/deref_test.c
diff --git a/mm/kwatch/.kunitconfig b/mm/kwatch/.kunitconfig
new file mode 100644
index 000000000000..7e977ddf0da1
--- /dev/null
+++ b/mm/kwatch/.kunitconfig
@@ -0,0 +1,9 @@
+CONFIG_KUNIT=y
+CONFIG_KWATCH=y
+CONFIG_KWATCH_KUNIT_TEST=y
+CONFIG_PERF_EVENTS=y
+CONFIG_HAVE_HW_BREAKPOINT=y
+CONFIG_HAVE_REINSTALL_HW_BREAKPOINT=y
+CONFIG_KPROBES=y
+CONFIG_KRETPROBES=y
+CONFIG_PRINTK=y
diff --git a/mm/kwatch/Kconfig b/mm/kwatch/Kconfig
index 9daf6d4463ef..6ec9aa448ece 100644
--- a/mm/kwatch/Kconfig
+++ b/mm/kwatch/Kconfig
@@ -14,3 +14,15 @@ config KWATCH
exact instruction causing the illegal access.
If unsure, say N.
+
+config KWATCH_KUNIT_TEST
+ bool "KUnit tests for KWatch" if !KUNIT_ALL_TESTS
+ # Built into the kwatch module, so it must be y; a bool cannot be
+ # enabled when KWATCH is a module (KWATCH=m would force it off).
+ depends on KWATCH=y && KUNIT
+ default KUNIT_ALL_TESTS
+ help
+ Enable KUnit tests for the KWatch kernel module.
+ This suite tests the core parsing logic, the pointer-chasing
+ finite state machine, and edge cases involving complex watchpoint
+ expressions. If unsure, say N.
diff --git a/mm/kwatch/Makefile b/mm/kwatch/Makefile
index 02d7917602f1..1d223d73b461 100644
--- a/mm/kwatch/Makefile
+++ b/mm/kwatch/Makefile
@@ -1,3 +1,4 @@
obj-$(CONFIG_KWATCH) += kwatch.o
kwatch-y := core.o deref.o task_ctx.o hwbp.o probe.o anchor.o
+kwatch-$(CONFIG_KWATCH_KUNIT_TEST) += deref_test.o
diff --git a/mm/kwatch/deref_test.c b/mm/kwatch/deref_test.c
new file mode 100644
index 000000000000..35919dd24d92
--- /dev/null
+++ b/mm/kwatch/deref_test.c
@@ -0,0 +1,146 @@
+// SPDX-License-Identifier: GPL-2.0
+#include <kunit/test.h>
+#include "kwatch.h"
+#include <linux/string.h>
+
+static void kwatch_test_parse_deref_chain(struct kunit *test)
+{
+ struct kwatch_config cfg;
+ int ret;
+
+ // Test 1: stack
+ memset(&cfg, 0, sizeof(cfg));
+ ret = kwatch_deref_parse(&cfg, "stack");
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ KUNIT_EXPECT_EQ(test, cfg.base, KWATCH_BASE_STACK);
+ KUNIT_EXPECT_EQ(test, cfg.offset_count, 1);
+ KUNIT_EXPECT_EQ(test, cfg.offsets[0], 0);
+
+ // Test 2: arg1
+ memset(&cfg, 0, sizeof(cfg));
+ ret = kwatch_deref_parse(&cfg, "arg1");
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ KUNIT_EXPECT_EQ(test, cfg.base, KWATCH_BASE_ARG1);
+ KUNIT_EXPECT_EQ(test, cfg.offset_count, 1);
+ KUNIT_EXPECT_EQ(test, cfg.offsets[0], 0);
+
+ // Test 3: arg6+8
+ memset(&cfg, 0, sizeof(cfg));
+ ret = kwatch_deref_parse(&cfg, "arg6+8");
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ KUNIT_EXPECT_EQ(test, cfg.base, KWATCH_BASE_ARG6);
+ KUNIT_EXPECT_EQ(test, cfg.offset_count, 1);
+ KUNIT_EXPECT_EQ(test, cfg.offsets[0], 8);
+
+ // Test 4: arg2-16
+ memset(&cfg, 0, sizeof(cfg));
+ ret = kwatch_deref_parse(&cfg, "arg2-16");
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ KUNIT_EXPECT_EQ(test, cfg.base, KWATCH_BASE_ARG2);
+ KUNIT_EXPECT_EQ(test, cfg.offset_count, 1);
+ KUNIT_EXPECT_EQ(test, cfg.offsets[0], -16);
+
+ // Test 5: arg3->8
+ memset(&cfg, 0, sizeof(cfg));
+ ret = kwatch_deref_parse(&cfg, "arg3->8");
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ KUNIT_EXPECT_EQ(test, cfg.base, KWATCH_BASE_ARG3);
+ KUNIT_EXPECT_EQ(test, cfg.offset_count, 2);
+ KUNIT_EXPECT_EQ(test, cfg.offsets[0], 0);
+ KUNIT_EXPECT_EQ(test, cfg.offsets[1], 8);
+
+ // Test 6: arg4+8->16
+ memset(&cfg, 0, sizeof(cfg));
+ ret = kwatch_deref_parse(&cfg, "arg4+8->16");
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ KUNIT_EXPECT_EQ(test, cfg.base, KWATCH_BASE_ARG4);
+ KUNIT_EXPECT_EQ(test, cfg.offset_count, 2);
+ KUNIT_EXPECT_EQ(test, cfg.offsets[0], 8);
+ KUNIT_EXPECT_EQ(test, cfg.offsets[1], 16);
+
+ // Test 7: arg5-8->-16
+ memset(&cfg, 0, sizeof(cfg));
+ ret = kwatch_deref_parse(&cfg, "arg5-8->-16");
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ KUNIT_EXPECT_EQ(test, cfg.base, KWATCH_BASE_ARG5);
+ KUNIT_EXPECT_EQ(test, cfg.offset_count, 2);
+ KUNIT_EXPECT_EQ(test, cfg.offsets[0], -8);
+ KUNIT_EXPECT_EQ(test, cfg.offsets[1], -16);
+
+ // Test 8: stack->0->8
+ memset(&cfg, 0, sizeof(cfg));
+ ret = kwatch_deref_parse(&cfg, "stack->0->8");
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ KUNIT_EXPECT_EQ(test, cfg.base, KWATCH_BASE_STACK);
+ KUNIT_EXPECT_EQ(test, cfg.offset_count, 3);
+ KUNIT_EXPECT_EQ(test, cfg.offsets[0], 0);
+ KUNIT_EXPECT_EQ(test, cfg.offsets[1], 0);
+ KUNIT_EXPECT_EQ(test, cfg.offsets[2], 8);
+
+ // Test 9: arg1->+8
+ memset(&cfg, 0, sizeof(cfg));
+ ret = kwatch_deref_parse(&cfg, "arg1->+8");
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ KUNIT_EXPECT_EQ(test, cfg.base, KWATCH_BASE_ARG1);
+ KUNIT_EXPECT_EQ(test, cfg.offset_count, 2);
+ KUNIT_EXPECT_EQ(test, cfg.offsets[0], 0);
+ KUNIT_EXPECT_EQ(test, cfg.offsets[1], 8);
+
+ // Test 9.1: arg1-> (implicit 0 should fail)
+ memset(&cfg, 0, sizeof(cfg));
+ ret = kwatch_deref_parse(&cfg, "arg1->");
+ KUNIT_EXPECT_EQ(test, ret, -EINVAL);
+
+ // Test 9.2: stack->->8 (implicit 0 should fail)
+ memset(&cfg, 0, sizeof(cfg));
+ ret = kwatch_deref_parse(&cfg, "stack->->8");
+ KUNIT_EXPECT_EQ(test, ret, -EINVAL);
+
+ // Test 10: Invalid base
+ memset(&cfg, 0, sizeof(cfg));
+ ret = kwatch_deref_parse(&cfg, "invalid_base");
+ KUNIT_EXPECT_EQ(test, ret, -EINVAL);
+
+ // Test 11: Invalid offset
+ memset(&cfg, 0, sizeof(cfg));
+ ret = kwatch_deref_parse(&cfg, "arg1+abc");
+ KUNIT_EXPECT_EQ(test, ret, -EINVAL);
+
+ // Test 12: Invalid arg
+ memset(&cfg, 0, sizeof(cfg));
+ ret = kwatch_deref_parse(&cfg, "arg7");
+ KUNIT_EXPECT_EQ(test, ret, -EINVAL);
+
+ // Test 13: Absolute address. Use a width-appropriate literal: a 64-bit
+ // address would overflow unsigned long and fail kstrtoul() on 32-bit.
+ memset(&cfg, 0, sizeof(cfg));
+#if BITS_PER_LONG == 64
+ ret = kwatch_deref_parse(&cfg, "0xffffffff81000000+8");
+#else
+ ret = kwatch_deref_parse(&cfg, "0xc1000000+8");
+#endif
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ KUNIT_EXPECT_EQ(test, cfg.base, KWATCH_BASE_ABS_ADDR);
+#if BITS_PER_LONG == 64
+ KUNIT_EXPECT_EQ(test, cfg.sym_addr, 0xffffffff81000000UL);
+#else
+ KUNIT_EXPECT_EQ(test, cfg.sym_addr, 0xc1000000UL);
+#endif
+ KUNIT_EXPECT_EQ(test, cfg.offset_count, 1);
+ KUNIT_EXPECT_EQ(test, cfg.offsets[0], 8);
+}
+
+static struct kunit_case kwatch_deref_test_cases[] = {
+ KUNIT_CASE(kwatch_test_parse_deref_chain),
+ {}
+};
+
+static struct kunit_suite kwatch_deref_test_suite = {
+ .name = "kwatch_deref",
+ .test_cases = kwatch_deref_test_cases,
+};
+
+kunit_test_suite(kwatch_deref_test_suite);
+
+MODULE_DESCRIPTION("KUnit tests for the KWatch watch expression parser");
+MODULE_LICENSE("GPL");
--
2.53.0
^ permalink raw reply related [flat|nested] 24+ messages in thread* [RFC PATCH v2 13/13] Documentation/dev-tools: document KWatch
2026-07-17 12:50 [RFC PATCH v2 00/13] mm/kwatch: dynamic hardware watchpoints for hunting memory corruption Jinchao Wang
` (11 preceding siblings ...)
2026-07-17 13:07 ` [RFC PATCH v2 12/13] mm/kwatch: add KUnit tests for the watch expression parser Jinchao Wang
@ 2026-07-17 13:07 ` Jinchao Wang
2026-07-17 13:30 ` sashiko-bot
2026-07-17 13:41 ` [RFC PATCH v2 00/13] mm/kwatch: dynamic hardware watchpoints for hunting memory corruption Dave Hansen
13 siblings, 1 reply; 24+ messages in thread
From: Jinchao Wang @ 2026-07-17 13:07 UTC (permalink / raw)
To: Andrew Morton, Peter Zijlstra, Thomas Gleixner, Steven Rostedt,
Masami Hiramatsu
Cc: Ingo Molnar, Borislav Petkov, Dave Hansen, H . Peter Anvin, x86,
Arnaldo Carvalho de Melo, Namhyung Kim, Mark Rutland,
Mathieu Desnoyers, David Hildenbrand, Jonathan Corbet,
Matthew Wilcox, Alan Stern, Randy Dunlap, Alexander Potapenko,
Marco Elver, Mike Rapoport, linux-kernel, linux-mm,
linux-trace-kernel, linux-perf-users, linux-doc, Jinchao Wang
Describe what KWatch is for, how it compares with KASAN and KFENCE,
the debugfs configuration interface, the watch expression syntax,
how to read hits from the trace buffer (including after a crash),
and the current limitations.
Signed-off-by: Jinchao Wang <wangjinchao600@gmail.com>
---
Documentation/dev-tools/index.rst | 1 +
Documentation/dev-tools/kwatch.rst | 207 +++++++++++++++++++++++++++++
2 files changed, 208 insertions(+)
create mode 100644 Documentation/dev-tools/kwatch.rst
diff --git a/Documentation/dev-tools/index.rst b/Documentation/dev-tools/index.rst
index 59cbb77b33ff..f4c748da63db 100644
--- a/Documentation/dev-tools/index.rst
+++ b/Documentation/dev-tools/index.rst
@@ -30,6 +30,7 @@ Documentation/process/debugging/index.rst
ubsan
kmemleak
kcsan
+ kwatch
lkmm/index
kfence
kselftest
diff --git a/Documentation/dev-tools/kwatch.rst b/Documentation/dev-tools/kwatch.rst
new file mode 100644
index 000000000000..e58f3185ebbd
--- /dev/null
+++ b/Documentation/dev-tools/kwatch.rst
@@ -0,0 +1,207 @@
+.. SPDX-License-Identifier: GPL-2.0
+
+======================================
+KWatch - Kernel Memory Watchpoint Tool
+======================================
+
+Overview
+========
+
+KWatch is a runtime-configurable debugging tool for locating kernel memory
+corruption. It arms hardware breakpoints (watchpoints) on a target address
+while a chosen function is executing, and reports the exact instruction that
+touches the watched memory, together with a stack trace, through a
+tracepoint.
+
+Unlike shadow-memory sanitizers, KWatch does not detect invalid accesses in
+general; it answers a narrower but common question during corruption hunts:
+"who writes to this address?". This includes in-bounds logical overwrites
+that KASAN cannot see, because the rogue writer modifies valid memory
+through a valid pointer, just at the wrong time or with the wrong data.
+
+Comparison with other tools:
+
+* KASAN detects out-of-bounds and use-after-free accesses, but reports the
+ symptom (the invalid access), not the writer that corrupted the data
+ earlier. It requires a rebuild and has significant CPU and memory
+ overhead, and its redzones perturb memory layout, which can hide
+ timing-sensitive bugs.
+* KFENCE is a low-overhead sampling detector for slab objects; it cannot be
+ pointed at one specific address.
+* Hardware breakpoints via kgdb or perf can watch an address, but only a
+ fixed one, system-wide, for the whole run. KWatch resolves the address
+ dynamically at function entry (for example "argument 2 of this function,
+ plus offset 8, dereferenced once") and disarms it again at function exit,
+ so short-lived and per-invocation objects can be watched too.
+
+KWatch has near-zero overhead while armed: the watched function pays for
+one kprobe/kretprobe pair plus programming of the debug registers; the rest
+of the system runs at full speed.
+
+Requirements
+============
+
+* ``CONFIG_KWATCH=y`` or ``m``. The Kconfig symbol depends on
+ ``CONFIG_PERF_EVENTS``, ``CONFIG_DEBUG_FS`` and an architecture that
+ provides ``HAVE_REINSTALL_HW_BREAKPOINT`` (currently x86 only).
+* Resolving symbol names in watch expressions requires ``CONFIG_KWATCH=y``
+ (built-in); a module can only watch absolute hexadecimal addresses.
+
+Usage
+=====
+
+KWatch is configured through a single debugfs file::
+
+ /sys/kernel/debug/kwatch/config
+
+Writing a configuration string starts a watch session (stopping any previous
+one); reading the file shows the active configuration and hit-rejection
+counters. The configuration is a whitespace-separated list of ``key=value``
+tokens:
+
+=================== ==========================================================
+Key Meaning
+=================== ==========================================================
+``func_name`` Function whose execution opens the watch window.
+``func_offset`` Instruction offset inside ``func_name`` at which the
+ watchpoint is armed (default 0 = function entry).
+``watch_expr`` Expression describing the address to watch (see below).
+``watch_len`` Watched length in bytes: 1, 2, 4 or 8 (default 8).
+``depth`` Recursion depth at which the window opens (default 0).
+``max_watch`` Number of hardware watchpoints to preallocate
+ (default 4).
+``max_concurrency`` Maximum number of tasks concurrently inside the watch
+ window (default 256).
+``duration`` For global watches: seconds until automatic stop.
+=================== ==========================================================
+
+Watch expressions
+-----------------
+
+The address to watch is computed at function entry from::
+
+ watch_expr={base}[+-offset][->[+-]offset]...
+
+* ``base`` is one of:
+
+ - ``arg1`` ... ``arg6``: a function argument (register calling
+ convention). These are only meaningful at function entry, i.e. with
+ ``func_offset`` unset; combining ``argN`` with ``func_offset`` reads
+ the argument registers mid-function, where they no longer hold the
+ original arguments,
+ - ``stack``: the kernel stack pointer at the probe point,
+ - an absolute hexadecimal address, e.g. ``0xffffffff81234567``,
+ - a global symbol name (built-in KWatch only).
+
+* ``+offset`` / ``-offset`` adjusts the current address.
+* ``->offset`` loads the pointer stored at the current address (via
+ ``get_kernel_nofault()``) and then applies the offset. Up to four chain
+ elements are supported; offsets must be explicit (``->`` alone is
+ rejected).
+
+Given::
+
+ struct some_struct {
+ struct some_struct *ptr; /* offset 0 */
+ int num; /* offset 8 */
+ };
+
+ void target_function(struct some_struct *arg1);
+
+typical expressions are:
+
+=========================== ==============================================
+Expression Watches
+=========================== ==============================================
+``watch_expr=arg1`` ``&arg1->ptr`` (the pointer field itself)
+``watch_expr=arg1+8`` ``&arg1->num``
+``watch_expr=arg1->0`` ``&arg1->ptr->ptr`` (one dereference)
+``watch_expr=arg1->8`` ``&arg1->ptr->num``
+``watch_expr=0xffff...+8`` absolute address plus 8
+=========================== ==============================================
+
+Example: catch whoever overwrites ``arg1->num`` of a function while that
+function runs::
+
+ echo "func_name=target_function watch_expr=arg1+8 watch_len=4" \
+ > /sys/kernel/debug/kwatch/config
+
+Watching global variables
+-------------------------
+
+A global variable has no natural function window. When ``duration`` is
+given without ``func_name``, KWatch starts an internal anchor kernel thread
+that sleeps inside a dummy function, and uses that function as the window::
+
+ echo "watch_expr=jiffies_wobble duration=60 watch_len=8" \
+ > /sys/kernel/debug/kwatch/config
+
+The session tears itself down when the duration expires.
+
+Reading hits
+------------
+
+Hits are emitted as the ``kwatch:kwatch_hit`` tracepoint, which is safe in
+NMI-like contexts where printk is not. Each event carries the timestamp,
+the instruction pointer, the watched address and a short stack trace::
+
+ echo 1 > /sys/kernel/debug/tracing/events/kwatch/kwatch_hit/enable
+ cat /sys/kernel/debug/tracing/trace_pipe
+
+If the corruption crashes the machine, the ring buffer can still be
+recovered:
+
+* ``echo 1 > /proc/sys/kernel/ftrace_dump_on_oops`` (or the
+ ``ftrace_dump_on_oops`` boot parameter) dumps the buffer to the console
+ on an oops.
+* With kdump, the buffer is present in the vmcore and can be read with
+ ``crash> trace``.
+* ``CONFIG_PSTORE_FTRACE`` persists it across reboots on supported
+ platforms.
+
+Limitations
+===========
+
+* Functions that run in a genuine NMI(-like) context are rejected at
+ function entry; rejected invocations never open a watch window and are
+ counted in the ``nmi_rejected`` field of the config file. Watching
+ functions reachable from NMI handlers is out of scope.
+* The number of concurrent watchpoints is bounded by the CPU's debug
+ registers (typically 4).
+* Cross-CPU re-arming of a watchpoint is rate-limited per CPU: the local
+ CPU is always re-pointed, but the broadcast to other CPUs is throttled
+ so a very hot watched function cannot storm the system with IPIs. While
+ a broadcast is suppressed the other CPUs keep watching the previous
+ address, so a writer that runs on another CPU during that window can be
+ missed; the number of suppressed broadcasts is reported in the
+ ``arm_ipi_suppressed`` field of the config file. KWatch therefore
+ targets functions that are entered at a moderate rate.
+* If the target address cannot be resolved at arming time (for example a
+ ``get_kernel_nofault()`` failure on a swapped or unmapped page), the
+ watchpoint is not armed for that invocation.
+* Offsets in watch expressions are static; dynamic indexing such as
+ ``arg1->ptr[arg2]`` is not supported.
+* If a task is torn down while still inside the watched function without
+ the function returning (an oops or BUG in the window, which abandons the
+ stack), its watch window is not closed until the session stops. This is
+ the same best-effort cleanup that applies to every resource a task holds
+ when it dies abnormally. Do not target the task-exit path itself.
+* arm64 is not yet supported: stepping over a hit that has a custom
+ overflow handler needs a generic mechanism in the arch code, which is
+ planned as a follow-up series.
+
+Implementation notes
+====================
+
+The implementation lives in ``mm/kwatch/`` and is split into a control
+plane (``core.c``, the debugfs interface), an execution plane (``probe.c``
+and ``deref.c``: kprobe/kretprobe window management and address
+resolution), and a resource plane (``hwbp.c`` and ``task_ctx.c``).
+
+Hardware watchpoints are preallocated as perf events on every CPU and
+re-pointed at hit time with ``modify_wide_hw_breakpoint_local()``, a new
+hw_breakpoint API that updates the breakpoint on the local CPU without
+releasing its slot; other CPUs are updated by asynchronous IPIs. Per-task
+window state is kept in a fixed-size, lockless open-addressing array
+claimed with ``cmpxchg()``, so the hit path performs no allocation and
+takes no locks, which keeps it safe in atomic and NMI-like contexts.
--
2.53.0
^ permalink raw reply related [flat|nested] 24+ messages in thread* Re: [RFC PATCH v2 13/13] Documentation/dev-tools: document KWatch
2026-07-17 13:07 ` [RFC PATCH v2 13/13] Documentation/dev-tools: document KWatch Jinchao Wang
@ 2026-07-17 13:30 ` sashiko-bot
0 siblings, 0 replies; 24+ messages in thread
From: sashiko-bot @ 2026-07-17 13:30 UTC (permalink / raw)
To: Jinchao Wang; +Cc: linux-perf-users
Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
- [High] Race condition during CPU offline leads to spurious WARN_ONCE and potential Use-After-Free.
- [High] KWatch blindly filters out hits from the armed task's callees and local interrupts.
- [Medium] Missing Kconfig dependency on TRACING/TRACEPOINTS.
--
--- Patch [13]: [RFC PATCH v2 13/13] Documentation/dev-tools: document KWatch ---
commit 2276ea12010818a130be33709ca8b2c8928c26a9
Author: Jinchao Wang <wangjinchao600@gmail.com>
Documentation/dev-tools: document KWatch
Describe what KWatch is for, how it compares with KASAN and KFENCE,
the debugfs configuration interface, the watch expression syntax,
how to read hits from the trace buffer (including after a crash),
and the current limitations.
> diff --git a/mm/kwatch/Kconfig b/mm/kwatch/Kconfig
> new file mode 100644
> index 0000000000000..6ec9aa448ecec
> --- /dev/null
> +++ b/mm/kwatch/Kconfig
> @@ -0,0 +1,28 @@
> +config KWATCH
> + tristate "Kernel Watch Framework"
> + depends on PERF_EVENTS && HAVE_HW_BREAKPOINT && DEBUG_FS
> + depends on HAVE_REINSTALL_HW_BREAKPOINT
> + depends on KPROBES && KRETPROBES
> + depends on STACKTRACE
[Severity: Medium]
Should this configuration depend on or select TRACING or TRACEPOINTS?
Since KWatch outputs successful memory access hits exclusively through the
kwatch_hit tracepoint, building without tracepoint support results in KWatch
compiling and consuming hardware debug registers, but silently failing to
report any hits when memory corruption occurs.
> + help
> + A generalized hardware-assisted memory monitor utility.
[ ... ]
> diff --git a/mm/kwatch/hwbp.c b/mm/kwatch/hwbp.c
> new file mode 100644
> index 0000000000000..d1e93754cce82
> --- /dev/null
> +++ b/mm/kwatch/hwbp.c
[ ... ]
> +static int kwatch_hwbp_cpu_offline(unsigned int cpu)
> +{
> + struct kwatch_watchpoint *wp;
> + struct perf_event *bp;
> +
> + mutex_lock(&kwatch_all_wp_mutex);
> + list_for_each_entry(wp, &kwatch_all_wp_list, list) {
> + bp = per_cpu(*wp->event, cpu);
> + if (bp) {
> + unregister_hw_breakpoint(bp);
> + per_cpu(*wp->event, cpu) = NULL;
[Severity: High]
Does this sequence open a race condition during CPU offline?
If an asynchronous IPI from kwatch_hwbp_arm() interrupts the offlining CPU
exactly between the unregistration and the pointer being cleared:
unregister_hw_breakpoint(bp);
/* IPI arrives here */
per_cpu(*wp->event, cpu) = NULL;
The IPI handler in kwatch_hwbp_arm_local() will read the non-NULL,
unregistered breakpoint and pass it to modify_wide_hw_breakpoint_local().
Could this access unlinked and potentially freed memory (if the RCU grace
period elapses), corrupting debug registers or triggering a spurious
WARN_ONCE?
> + }
> + }
> + mutex_unlock(&kwatch_all_wp_mutex);
> + return 0;
> +}
[ ... ]
> diff --git a/mm/kwatch/probe.c b/mm/kwatch/probe.c
> new file mode 100644
> index 0000000000000..249aa50c9f782
> --- /dev/null
> +++ b/mm/kwatch/probe.c
[ ... ]
> +bool kwatch_probe_validate_hit(struct pt_regs *regs,
> + struct task_struct *arm_tsk)
> +{
> + struct kwatch_tsk_ctx *ctx = kwatch_tsk_ctx_get(false);
> + const struct kwatch_config *cfg = kwatch_probe_ctx.cfg;
> +
> + if (unlikely(!ctx || !cfg))
> + return true;
> +
> + if (arm_tsk != current || ctx->depth != cfg->depth + 1)
> + return true;
> +
> + return false;
[Severity: High]
Does this logic inadvertently filter out genuine corruptions?
Returning false when arm_tsk == current and ctx->depth == cfg->depth + 1
prevents hits from being logged for the current function. However, since
depth is only incremented when recursively entering the watched function,
does this also silently ignore memory corruptions caused by the watched
function's callees, or by any interrupt handler that preempts the watched
task on the same CPU?
> +}
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260717125023.1895892-1-wangjinchao600@gmail.com?part=13
^ permalink raw reply [flat|nested] 24+ messages in thread
* Re: [RFC PATCH v2 00/13] mm/kwatch: dynamic hardware watchpoints for hunting memory corruption
2026-07-17 12:50 [RFC PATCH v2 00/13] mm/kwatch: dynamic hardware watchpoints for hunting memory corruption Jinchao Wang
` (12 preceding siblings ...)
2026-07-17 13:07 ` [RFC PATCH v2 13/13] Documentation/dev-tools: document KWatch Jinchao Wang
@ 2026-07-17 13:41 ` Dave Hansen
13 siblings, 0 replies; 24+ messages in thread
From: Dave Hansen @ 2026-07-17 13:41 UTC (permalink / raw)
To: Jinchao Wang, Andrew Morton, Peter Zijlstra, Thomas Gleixner,
Steven Rostedt, Masami Hiramatsu
Cc: Ingo Molnar, Borislav Petkov, Dave Hansen, H . Peter Anvin, x86,
Arnaldo Carvalho de Melo, Namhyung Kim, Mark Rutland,
Mathieu Desnoyers, David Hildenbrand, Jonathan Corbet,
Matthew Wilcox, Alan Stern, Randy Dunlap, Alexander Potapenko,
Marco Elver, Mike Rapoport, linux-kernel, linux-mm,
linux-trace-kernel, linux-perf-users, linux-doc
On 7/17/26 05:50, Jinchao Wang wrote:
> 24 files changed, 2115 insertions(+), 63 deletions(-)
Reading this, I wonder how many kernel debugging features we need. I
don't even think we have a centralized list of them. They all just live
in their own silos.
This one really seems like a super specialized tool. It has to be
enabled at compile time and specifically aimed at a specific function.
Maybe this should live off on the side for a while. If folks end up
actually needing it, they can point their friendly LLM over to its tree.
^ permalink raw reply [flat|nested] 24+ messages in thread