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 <[email protected]>
---
 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 <[email protected]>
+L:     [email protected]
+S:     Maintained
+F:     Documentation/dev-tools/kwatch.rst
+F:     include/trace/events/kwatch.h
+F:     mm/kwatch/
+
 L3MDEV
 M:     David Ahern <[email protected]>
 L:     [email protected]
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 <[email protected]>");
+MODULE_DESCRIPTION("Kernel watchpoint");
+MODULE_LICENSE("GPL");
-- 
2.53.0


Reply via email to