[RFC PATCH 05/13] mm/kwatch: add watch expression parser and dereference engine
From: Jinchao Wang <hidden>
Date: 2026-07-14 18:31:21
Also in:
linux-doc, linux-mm, linux-perf-users, lkml
Subsystem:
memory management, the rest · Maintainers:
Andrew Morton, Linus Torvalds
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 <redacted>
---
mm/kwatch/Makefile | 3 +
mm/kwatch/deref.c | 174 +++++++++++++++++++++++++++++++++++++++++++++
mm/kwatch/kwatch.h | 107 ++++++++++++++++++++++++++++
3 files changed, 284 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..e1ac8ae312f6
--- /dev/null
+++ b/mm/kwatch/kwatch.h@@ -0,0 +1,107 @@ +/* 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_access_type { + KWATCH_ACCESS_W, + KWATCH_ACCESS_R, + KWATCH_ACCESS_RW, + KWATCH_ACCESS_X, +}; + +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; + enum kwatch_access_type access_type; + 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, enum kwatch_access_type access_type); +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); + +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_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