[RFC PATCH v2 05/13] mm/kwatch: add watch expression parser and dereference engine
From: Jinchao Wang
Date: Fri Jul 17 2026 - 09:08:48 EST
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@xxxxxxxxx>
---
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