Provide ksw_stack_init() and ksw_stack_exit() to manage entry and
exit probes for the target function from ksw_get_config().
Use atomic PID tracking to ensure singleton watch.
Signed-off-by: Jinchao Wang <wangjinchao600@gmail.com>
---
mm/kstackwatch/kstackwatch.h | 4 ++
mm/kstackwatch/stack.c | 99 ++++++++++++++++++++++++++++++++++++
2 files changed, 103 insertions(+)
diff --git a/mm/kstackwatch/kstackwatch.h b/mm/kstackwatch/kstackwatch.h
index 2fa377843f17..79ca40e69268 100644
--- a/mm/kstackwatch/kstackwatch.h
+++ b/mm/kstackwatch/kstackwatch.h
@@ -38,6 +38,10 @@ struct ksw_config {
// singleton, only modified in kernel.c
const struct ksw_config *ksw_get_config(void);
+/* stack management */
+int ksw_stack_init(void);
+void ksw_stack_exit(void);
+
/* watch management */
int ksw_watch_init(void);
void ksw_watch_exit(void);
diff --git a/mm/kstackwatch/stack.c b/mm/kstackwatch/stack.c
index cec594032515..72409156458f 100644
--- a/mm/kstackwatch/stack.c
+++ b/mm/kstackwatch/stack.c
@@ -1 +1,100 @@
// SPDX-License-Identifier: GPL-2.0
+#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
+
+#include <linux/atomic.h>
+#include <linux/fprobe.h>
+#include <linux/kprobes.h>
+#include <linux/printk.h>
+#include <linux/spinlock.h>
+
+#include "kstackwatch.h"
+
+static struct kprobe entry_probe;
+static struct fprobe exit_probe;
+#define INVALID_PID -1
+static atomic_t ksw_stack_pid = ATOMIC_INIT(INVALID_PID);
+
+static int ksw_stack_prepare_watch(struct pt_regs *regs,
+ const struct ksw_config *config,
+ u64 *watch_addr, u64 *watch_len)
+{
+ /* implement logic will be added in following patches */
+ *watch_addr = 0;
+ *watch_len = 0;
+ return 0;
+}
+
+static void ksw_stack_entry_handler(struct kprobe *p, struct pt_regs *regs,
+ unsigned long flags)
+{
+ u64 watch_addr;
+ u64 watch_len;
+ int ret;
+
+ if (atomic_cmpxchg(&ksw_stack_pid, INVALID_PID, current->pid) !=
+ INVALID_PID)
+ return;
+
+ ret = ksw_stack_prepare_watch(regs, ksw_get_config(), &watch_addr,
+ &watch_len);
+ if (ret) {
+ atomic_set(&ksw_stack_pid, INVALID_PID);
+ pr_err("failed to prepare watch target: %d\n", ret);
+ return;
+ }
+
+ ret = ksw_watch_on(watch_addr, watch_len);
+ if (ret) {
+ atomic_set(&ksw_stack_pid, INVALID_PID);
+ pr_err("failed to watch on addr:0x%llx len:%llu %d\n",
+ watch_addr, watch_len, ret);
+ return;
+ }
+}
+
+static void ksw_stack_exit_handler(struct fprobe *fp, unsigned long ip,
+ unsigned long ret_ip,
+ struct ftrace_regs *regs, void *data)
+{
+ if (atomic_read(&ksw_stack_pid) != current->pid)
+ return;
+
+ ksw_watch_off();
+
+ atomic_set(&ksw_stack_pid, INVALID_PID);
+}
+
+int ksw_stack_init(void)
+{
+ int ret;
+ char *symbuf = NULL;
+
+ memset(&entry_probe, 0, sizeof(entry_probe));
+ entry_probe.symbol_name = ksw_get_config()->function;
+ entry_probe.offset = ksw_get_config()->ip_offset;
+ entry_probe.post_handler = ksw_stack_entry_handler;
+ ret = register_kprobe(&entry_probe);
+ if (ret) {
+ pr_err("Failed to register kprobe ret %d\n", ret);
+ return ret;
+ }
+
+ memset(&exit_probe, 0, sizeof(exit_probe));
+ exit_probe.exit_handler = ksw_stack_exit_handler;
+ symbuf = (char *)ksw_get_config()->function;
+
+ ret = register_fprobe_syms(&exit_probe, (const char **)&symbuf, 1);
+ if (ret < 0) {
+ pr_err("register_fprobe_syms fail %d\n", ret);
+ unregister_kprobe(&entry_probe);
+ return ret;
+ }
+
+ return 0;
+}
+
+void ksw_stack_exit(void)
+{
+ unregister_fprobe(&exit_probe);
+ unregister_kprobe(&entry_probe);
+}
--
2.43.0
Add helpers to find the stack canary or a local variable addr and len
for the probed function based on ksw_get_config(). For canary search,
limits search to a fixed number of steps to avoid scanning the entire
stack. Validates that the computed address and length are within the
kernel stack.
Signed-off-by: Jinchao Wang <wangjinchao600@gmail.com>
---
mm/kstackwatch/stack.c | 86 ++++++++++++++++++++++++++++++++++++++++--
1 file changed, 83 insertions(+), 3 deletions(-)
diff --git a/mm/kstackwatch/stack.c b/mm/kstackwatch/stack.c
index 72409156458f..3ea0f9de698e 100644
--- a/mm/kstackwatch/stack.c
+++ b/mm/kstackwatch/stack.c
@@ -13,14 +13,94 @@ static struct kprobe entry_probe;
static struct fprobe exit_probe;
#define INVALID_PID -1
static atomic_t ksw_stack_pid = ATOMIC_INIT(INVALID_PID);
+#define MAX_CANARY_SEARCH_STEPS 128
+
+static unsigned long ksw_find_stack_canary_addr(struct pt_regs *regs)
+{
+ unsigned long *stack_ptr, *stack_end, *stack_base;
+ unsigned long expected_canary;
+ unsigned int i;
+
+ stack_ptr = (unsigned long *)kernel_stack_pointer(regs);
+
+ stack_base = (unsigned long *)(current->stack);
+
+ // TODO: limit it to the current frame
+ stack_end = (unsigned long *)((char *)current->stack + THREAD_SIZE);
+
+ expected_canary = current->stack_canary;
+
+ if (stack_ptr < stack_base || stack_ptr >= stack_end) {
+ pr_err("Stack pointer 0x%lx out of bounds [0x%lx, 0x%lx)\n",
+ (unsigned long)stack_ptr, (unsigned long)stack_base,
+ (unsigned long)stack_end);
+ return 0;
+ }
+
+ for (i = 0; i < MAX_CANARY_SEARCH_STEPS; i++) {
+ if (&stack_ptr[i] >= stack_end)
+ break;
+
+ if (stack_ptr[i] == expected_canary) {
+ pr_debug("canary found i:%d 0x%lx\n", i,
+ (unsigned long)&stack_ptr[i]);
+ return (unsigned long)&stack_ptr[i];
+ }
+ }
+
+ pr_debug("canary not found in first %d steps\n",
+ MAX_CANARY_SEARCH_STEPS);
+ return 0;
+}
+
+static int ksw_stack_validate_addr(unsigned long addr, size_t size)
+{
+ unsigned long stack_start, stack_end;
+
+ if (!addr || !size)
+ return -EINVAL;
+
+ stack_start = (unsigned long)current->stack;
+ stack_end = stack_start + THREAD_SIZE;
+
+ if (addr < stack_start || (addr + size) > stack_end)
+ return -ERANGE;
+
+ return 0;
+}
static int ksw_stack_prepare_watch(struct pt_regs *regs,
const struct ksw_config *config,
u64 *watch_addr, u64 *watch_len)
{
- /* implement logic will be added in following patches */
- *watch_addr = 0;
- *watch_len = 0;
+ u64 addr;
+ u64 len;
+
+ /* Resolve addresses for all active watches */
+ switch (ksw_get_config()->type) {
+ case WATCH_CANARY:
+ addr = ksw_find_stack_canary_addr(regs);
+ len = sizeof(unsigned long);
+ break;
+
+ case WATCH_LOCAL_VAR:
+ addr = kernel_stack_pointer(regs) +
+ ksw_get_config()->local_var_offset;
+ len = ksw_get_config()->local_var_len;
+ break;
+
+ default:
+ pr_err("Unknown watch type %d\n", ksw_get_config()->type);
+ return -EINVAL;
+ }
+
+ if (ksw_stack_validate_addr(addr, len)) {
+ pr_err("invalid stack addr:0x%llx len :%llu\n", addr, len);
+ return -EINVAL;
+ }
+
+ *watch_addr = addr;
+ *watch_len = len;
return 0;
}
--
2.43.0
Track per-task recursion depth using a simple hashtable keyed by PID.
Entry/exit handlers update the depth, triggering only at the configured
recursion level.
Signed-off-by: Jinchao Wang <wangjinchao600@gmail.com>
---
mm/kstackwatch/stack.c | 100 ++++++++++++++++++++++++++++++++++++++++-
1 file changed, 98 insertions(+), 2 deletions(-)
diff --git a/mm/kstackwatch/stack.c b/mm/kstackwatch/stack.c
index 3ea0f9de698e..669876057f0b 100644
--- a/mm/kstackwatch/stack.c
+++ b/mm/kstackwatch/stack.c
@@ -3,6 +3,8 @@
#include <linux/atomic.h>
#include <linux/fprobe.h>
+#include <linux/hash.h>
+#include <linux/hashtable.h>
#include <linux/kprobes.h>
#include <linux/printk.h>
#include <linux/spinlock.h>
@@ -15,6 +17,83 @@ static struct fprobe exit_probe;
static atomic_t ksw_stack_pid = ATOMIC_INIT(INVALID_PID);
#define MAX_CANARY_SEARCH_STEPS 128
+struct depth_entry {
+ pid_t pid;
+ int depth; /* starts from 0 */
+ struct hlist_node node;
+};
+
+#define DEPTH_HASH_BITS 8
+#define DEPTH_HASH_SIZE BIT(DEPTH_HASH_BITS)
+static DEFINE_HASHTABLE(depth_hash, DEPTH_HASH_BITS);
+static DEFINE_SPINLOCK(depth_hash_lock);
+
+static int get_recursive_depth(void)
+{
+ struct depth_entry *entry;
+ pid_t pid = current->pid;
+ int depth = 0;
+
+ spin_lock(&depth_hash_lock);
+ hash_for_each_possible(depth_hash, entry, node, pid) {
+ if (entry->pid == pid) {
+ depth = entry->depth;
+ break;
+ }
+ }
+ spin_unlock(&depth_hash_lock);
+ return depth;
+}
+
+static void set_recursive_depth(int depth)
+{
+ struct depth_entry *entry;
+ pid_t pid = current->pid;
+ bool found = false;
+
+ spin_lock(&depth_hash_lock);
+ hash_for_each_possible(depth_hash, entry, node, pid) {
+ if (entry->pid == pid) {
+ entry->depth = depth;
+ found = true;
+ break;
+ }
+ }
+
+ if (found) {
+ // last exit handler
+ if (depth == 0) {
+ hash_del(&entry->node);
+ kfree(entry);
+ }
+ goto unlock;
+ }
+
+ WARN_ONCE(depth != 1, "new entry depth %d should be 1", depth);
+ entry = kmalloc(sizeof(*entry), GFP_ATOMIC);
+ if (entry) {
+ entry->pid = pid;
+ entry->depth = depth;
+ hash_add(depth_hash, &entry->node, pid);
+ }
+unlock:
+ spin_unlock(&depth_hash_lock);
+}
+
+static void reset_recursive_depth(void)
+{
+ struct depth_entry *entry;
+ struct hlist_node *tmp;
+ int bkt;
+
+ spin_lock(&depth_hash_lock);
+ hash_for_each_safe(depth_hash, bkt, tmp, entry, node) {
+ hash_del(&entry->node);
+ kfree(entry);
+ }
+ spin_unlock(&depth_hash_lock);
+}
+
static unsigned long ksw_find_stack_canary_addr(struct pt_regs *regs)
{
unsigned long *stack_ptr, *stack_end, *stack_base;
@@ -109,8 +188,15 @@ static void ksw_stack_entry_handler(struct kprobe *p, struct pt_regs *regs,
{
u64 watch_addr;
u64 watch_len;
+ int cur_depth;
int ret;
+ cur_depth = get_recursive_depth();
+ set_recursive_depth(cur_depth + 1);
+
+ if (cur_depth != ksw_get_config()->depth)
+ return;
+
if (atomic_cmpxchg(&ksw_stack_pid, INVALID_PID, current->pid) !=
INVALID_PID)
return;
@@ -126,8 +212,8 @@ static void ksw_stack_entry_handler(struct kprobe *p, struct pt_regs *regs,
ret = ksw_watch_on(watch_addr, watch_len);
if (ret) {
atomic_set(&ksw_stack_pid, INVALID_PID);
- pr_err("failed to watch on addr:0x%llx len:%llu %d\n",
- watch_addr, watch_len, ret);
+ pr_err("failed to watch on depth:%d addr:0x%llx len:%llu %d\n",
+ cur_depth, watch_addr, watch_len, ret);
return;
}
}
@@ -136,6 +222,14 @@ static void ksw_stack_exit_handler(struct fprobe *fp, unsigned long ip,
unsigned long ret_ip,
struct ftrace_regs *regs, void *data)
{
+ int cur_depth;
+
+ cur_depth = get_recursive_depth() - 1;
+ set_recursive_depth(cur_depth);
+
+ if (cur_depth != ksw_get_config()->depth)
+ return;
+
if (atomic_read(&ksw_stack_pid) != current->pid)
return;
@@ -149,6 +243,8 @@ int ksw_stack_init(void)
int ret;
char *symbuf = NULL;
+ reset_recursive_depth();
+
memset(&entry_probe, 0, sizeof(entry_probe));
entry_probe.symbol_name = ksw_get_config()->function;
entry_probe.offset = ksw_get_config()->ip_offset;
--
2.43.0
Introduce helper functions to start and stop watching the configured
function. These handle initialization/cleanup of both stack and watch
components, and maintain a `watching_active` flag to track current state.
Ensure procfs write triggers proper stop/start sequence, and show handler
indicates watching status.
Signed-off-by: Jinchao Wang <wangjinchao600@gmail.com>
---
mm/kstackwatch/kernel.c | 55 ++++++++++++++++++++++++++++++++++++++++-
1 file changed, 54 insertions(+), 1 deletion(-)
diff --git a/mm/kstackwatch/kernel.c b/mm/kstackwatch/kernel.c
index 8e1dca45003e..9ef969f28e29 100644
--- a/mm/kstackwatch/kernel.c
+++ b/mm/kstackwatch/kernel.c
@@ -17,6 +17,43 @@ MODULE_LICENSE("GPL");
static struct ksw_config *ksw_config;
static atomic_t config_file_busy = ATOMIC_INIT(0);
+static bool watching_active;
+
+static int ksw_start_watching(void)
+{
+ int ret;
+
+ /*
+ * Watch init will preallocate the HWBP,
+ * so it must happen before stack init
+ */
+ ret = ksw_watch_init();
+ if (ret) {
+ pr_err("ksw_watch_init ret: %d\n", ret);
+ return ret;
+ }
+
+ ret = ksw_stack_init();
+ if (ret) {
+ pr_err("ksw_stack_init ret: %d\n", ret);
+ ksw_watch_exit();
+ return ret;
+ }
+ watching_active = true;
+
+ pr_info("start watching: %s\n", ksw_config->config_str);
+ return 0;
+}
+
+static void ksw_stop_watching(void)
+{
+ ksw_stack_exit();
+ ksw_watch_exit();
+ watching_active = false;
+
+ pr_info("stop watching: %s\n", ksw_config->config_str);
+}
+
/*
* Format of the configuration string:
* function+ip_offset[+depth] [local_var_offset:local_var_len]
@@ -109,6 +146,9 @@ static ssize_t kstackwatch_proc_write(struct file *file,
if (copy_from_user(input, buffer, count))
return -EFAULT;
+ if (watching_active)
+ ksw_stop_watching();
+
input[count] = '\0';
strim(input);
@@ -123,12 +163,22 @@ static ssize_t kstackwatch_proc_write(struct file *file,
return ret;
}
+ ret = ksw_start_watching();
+ if (ret) {
+ pr_err("Failed to start watching with %d\n", ret);
+ return ret;
+ }
+
return count;
}
static int kstackwatch_proc_show(struct seq_file *m, void *v)
{
- seq_printf(m, "%s\n", ksw_config->config_str);
+ if (watching_active)
+ seq_printf(m, "%s\n", ksw_config->config_str);
+ else
+ seq_puts(m, "not watching\n");
+
return 0;
}
@@ -176,6 +226,9 @@ static int __init kstackwatch_init(void)
static void __exit kstackwatch_exit(void)
{
+ if (watching_active)
+ ksw_stop_watching();
+
remove_proc_entry("kstackwatch", NULL);
kfree(ksw_config);
--
2.43.0
Provide two debug helpers:
- ksw_watch_show(): print the current watch target address and length.
- ksw_watch_fire(): intentionally trigger the watchpoint immediately
by writing to the watched address, useful for testing HWBP behavior.
Signed-off-by: Jinchao Wang <wangjinchao600@gmail.com>
---
mm/kstackwatch/kstackwatch.h | 2 ++
mm/kstackwatch/watch.c | 18 ++++++++++++++++++
2 files changed, 20 insertions(+)
diff --git a/mm/kstackwatch/kstackwatch.h b/mm/kstackwatch/kstackwatch.h
index 79ca40e69268..8632b43b6a33 100644
--- a/mm/kstackwatch/kstackwatch.h
+++ b/mm/kstackwatch/kstackwatch.h
@@ -47,5 +47,7 @@ int ksw_watch_init(void);
void ksw_watch_exit(void);
int ksw_watch_on(u64 watch_addr, u64 watch_len);
void ksw_watch_off(void);
+void ksw_watch_show(void);
+void ksw_watch_fire(void);
#endif /* _KSTACKWATCH_H */
diff --git a/mm/kstackwatch/watch.c b/mm/kstackwatch/watch.c
index d95efefdffe9..87bbe54bb5d3 100644
--- a/mm/kstackwatch/watch.c
+++ b/mm/kstackwatch/watch.c
@@ -185,3 +185,21 @@ void ksw_watch_exit(void)
unregister_wide_hw_breakpoint(watch_events);
watch_events = NULL;
}
+
+/* self debug function */
+void ksw_watch_show(void)
+{
+ pr_info("watch target bp_addr: 0x%llx len:%llu\n", watch_attr.bp_addr,
+ watch_attr.bp_len);
+}
+EXPORT_SYMBOL_GPL(ksw_watch_show);
+
+/* self debug function */
+void ksw_watch_fire(void)
+{
+ char *ptr = (char *)watch_attr.bp_addr;
+
+ pr_warn("watch triggered immediately\n");
+ *ptr = 0x42; // This should trigger immediately for any bp_len
+}
+EXPORT_SYMBOL_GPL(ksw_watch_fire);
--
2.43.0
Introduce a separate test module to validate functionality in controlled
scenarios, such as stack canary writes and simulated corruption.
The module provides a proc interface (/proc/kstackwatch_test) that allows
triggering specific test cases via simple commands:
- test0: directly corrupt the canary to verify watch/fire behavior
Test module is built with optimizations disabled to ensure predictable
behavior.
Signed-off-by: Jinchao Wang <wangjinchao600@gmail.com>
---
mm/Kconfig.debug | 10 ++++
mm/kstackwatch/Makefile | 6 +++
mm/kstackwatch/test.c | 115 ++++++++++++++++++++++++++++++++++++++++
3 files changed, 131 insertions(+)
create mode 100644 mm/kstackwatch/test.c
diff --git a/mm/Kconfig.debug b/mm/Kconfig.debug
index fdfc6e6d0dec..46c280280980 100644
--- a/mm/Kconfig.debug
+++ b/mm/Kconfig.debug
@@ -320,3 +320,13 @@ config KSTACK_WATCH
the recursive depth of the monitored function.
If unsure, say N.
+
+config KSTACK_WATCH_TEST
+ tristate "KStackWatch Test Module"
+ depends on KSTACK_WATCH
+ help
+ This module provides controlled stack exhaustion and overflow scenarios
+ to verify the functionality of KStackWatch. It is particularly useful
+ for development and validation of the KStachWatch mechanism.
+
+ If unsure, say N.
diff --git a/mm/kstackwatch/Makefile b/mm/kstackwatch/Makefile
index 84a46cb9a766..d007b8dcd1c6 100644
--- a/mm/kstackwatch/Makefile
+++ b/mm/kstackwatch/Makefile
@@ -1,2 +1,8 @@
obj-$(CONFIG_KSTACK_WATCH) += kstackwatch.o
kstackwatch-y := kernel.o stack.o watch.o
+
+obj-$(CONFIG_KSTACK_WATCH_TEST) += kstackwatch_test.o
+kstackwatch_test-y := test.o
+CFLAGS_test.o := -fno-inline \
+ -fno-optimize-sibling-calls \
+ -fno-pic -fno-pie -O0 -Og
diff --git a/mm/kstackwatch/test.c b/mm/kstackwatch/test.c
new file mode 100644
index 000000000000..76dbfb042067
--- /dev/null
+++ b/mm/kstackwatch/test.c
@@ -0,0 +1,115 @@
+// SPDX-License-Identifier: GPL-2.0
+#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
+
+#include <linux/delay.h>
+#include <linux/kthread.h>
+#include <linux/module.h>
+#include <linux/prandom.h>
+#include <linux/printk.h>
+#include <linux/proc_fs.h>
+#include <linux/string.h>
+#include <linux/uaccess.h>
+
+#include "kstackwatch.h"
+
+MODULE_AUTHOR("Jinchao Wang");
+MODULE_DESCRIPTION("Simple KStackWatch Test Module");
+MODULE_LICENSE("GPL");
+
+static struct proc_dir_entry *test_proc;
+#define BUFFER_SIZE 4
+#define MAX_DEPTH 6
+
+/*
+ * Test Case 0: Write to the canary position directly (Canary Test)
+ * use a u64 buffer array to ensure the canary will be placed
+ * corrupt the stack canary using the debug function
+ */
+static void canary_test_write(void)
+{
+ u64 buffer[BUFFER_SIZE];
+
+ pr_info("starting %s\n", __func__);
+ ksw_watch_show();
+ ksw_watch_fire();
+
+ buffer[0] = 0;
+
+ /* make sure the compiler do not drop assign action */
+ barrier_data(buffer);
+ pr_info("canary write test completed\n");
+}
+
+static ssize_t test_proc_write(struct file *file, const char __user *buffer,
+ size_t count, loff_t *pos)
+{
+ char cmd[256];
+ int test_num;
+
+ if (count >= sizeof(cmd))
+ return -EINVAL;
+
+ if (copy_from_user(cmd, buffer, count))
+ return -EFAULT;
+
+ cmd[count] = '\0';
+ strim(cmd);
+
+ pr_info("received command: %s\n", cmd);
+
+ if (sscanf(cmd, "test%d", &test_num) == 1) {
+ switch (test_num) {
+ case 0:
+ pr_info("triggering canary write test\n");
+ canary_test_write();
+ break;
+ default:
+ pr_err("Unknown test number %d\n", test_num);
+ return -EINVAL;
+ }
+ } else {
+ pr_err("invalid command format. Use 'test1', 'test2', or 'test3'.\n");
+ return -EINVAL;
+ }
+
+ return count;
+}
+
+static ssize_t test_proc_read(struct file *file, char __user *buffer,
+ size_t count, loff_t *pos)
+{
+ static const char usage[] =
+ "KStackWatch Simplified Test Module\n"
+ "==================================\n"
+ "Usage:\n"
+ " echo 'test0' > /proc/kstackwatch_test - Canary write test\n";
+
+ return simple_read_from_buffer(buffer, count, pos, usage,
+ strlen(usage));
+}
+
+static const struct proc_ops test_proc_ops = {
+ .proc_read = test_proc_read,
+ .proc_write = test_proc_write,
+};
+
+static int __init kstackwatch_test_init(void)
+{
+ test_proc = proc_create("kstackwatch_test", 0600, NULL, &test_proc_ops);
+ if (!test_proc) {
+ pr_err("Failed to create proc entry\n");
+ return -ENOMEM;
+ }
+ pr_info("module loaded\n");
+ return 0;
+}
+
+static void __exit kstackwatch_test_exit(void)
+{
+ if (test_proc)
+ remove_proc_entry("kstackwatch_test", NULL);
+ pr_info("module unloaded\n");
+}
+
+module_init(kstackwatch_test_init);
+module_exit(kstackwatch_test_exit);
--
2.43.0
Extend the test module with a new test case (test1) that intentionally
overflows a local u64 buffer to corrupt the stack canary. This helps
validate detection of stack corruption under overflow conditions.
The proc interface is updated to document the new test:
- test1: stack canary overflow test
Signed-off-by: Jinchao Wang <wangjinchao600@gmail.com>
---
mm/kstackwatch/test.c | 28 +++++++++++++++++++++++++++-
1 file changed, 27 insertions(+), 1 deletion(-)
diff --git a/mm/kstackwatch/test.c b/mm/kstackwatch/test.c
index 76dbfb042067..ab1a3f92b5e8 100644
--- a/mm/kstackwatch/test.c
+++ b/mm/kstackwatch/test.c
@@ -40,6 +40,27 @@ static void canary_test_write(void)
pr_info("canary write test completed\n");
}
+/*
+ * Test Case 1: Stack Overflow (Canary Test)
+ * This function uses a u64 buffer 64-bit write
+ * to corrupt the stack canary with a single operation
+ */
+static void canary_test_overflow(void)
+{
+ u64 buffer[BUFFER_SIZE];
+
+ pr_info("starting %s\n", __func__);
+ pr_info("buffer 0x%lx\n", (unsigned long)buffer);
+
+ /* intentionally overflow the u64 buffer. */
+ ((u64 *)buffer + BUFFER_SIZE)[0] = 0xdeadbeefdeadbeef;
+
+ /* make sure the compiler do not drop assign action */
+ barrier_data(buffer);
+
+ pr_info("canary overflow test completed\n");
+}
+
static ssize_t test_proc_write(struct file *file, const char __user *buffer,
size_t count, loff_t *pos)
{
@@ -63,6 +84,10 @@ static ssize_t test_proc_write(struct file *file, const char __user *buffer,
pr_info("triggering canary write test\n");
canary_test_write();
break;
+ case 1:
+ pr_info("triggering canary overflow test\n");
+ canary_test_overflow();
+ break;
default:
pr_err("Unknown test number %d\n", test_num);
return -EINVAL;
@@ -82,7 +107,8 @@ static ssize_t test_proc_read(struct file *file, char __user *buffer,
"KStackWatch Simplified Test Module\n"
"==================================\n"
"Usage:\n"
- " echo 'test0' > /proc/kstackwatch_test - Canary write test\n";
+ " echo 'test0' > /proc/kstackwatch_test - Canary write test\n"
+ " echo 'test1' > /proc/kstackwatch_test - Canary overflow test\n";
return simple_read_from_buffer(buffer, count, pos, usage,
strlen(usage));
--
2.43.0
Introduce a new test scenario to simulate silent stack corruption:
- silent_corruption_buggy():
exposes a local variable address globally without resetting it.
- silent_corruption_unwitting():
reads the exposed pointer and modifies the memory, simulating a routine
that unknowingly writes to another stack frame.
- silent_corruption_victim():
demonstrates the effect of silent corruption on unrelated local variables.
Signed-off-by: Jinchao Wang <wangjinchao600@gmail.com>
---
mm/kstackwatch/test.c | 93 ++++++++++++++++++++++++++++++++++++++++++-
1 file changed, 92 insertions(+), 1 deletion(-)
diff --git a/mm/kstackwatch/test.c b/mm/kstackwatch/test.c
index ab1a3f92b5e8..b10465381089 100644
--- a/mm/kstackwatch/test.c
+++ b/mm/kstackwatch/test.c
@@ -20,6 +20,9 @@ static struct proc_dir_entry *test_proc;
#define BUFFER_SIZE 4
#define MAX_DEPTH 6
+/* global variables for Silent corruption test */
+static u64 *g_corrupt_ptr;
+
/*
* Test Case 0: Write to the canary position directly (Canary Test)
* use a u64 buffer array to ensure the canary will be placed
@@ -61,6 +64,89 @@ static void canary_test_overflow(void)
pr_info("canary overflow test completed\n");
}
+static void do_something(int min_ms, int max_ms)
+{
+ u32 rand;
+
+ get_random_bytes(&rand, sizeof(rand));
+ rand = min_ms + rand % (max_ms - min_ms + 1);
+ msleep(rand);
+}
+
+static void silent_corruption_buggy(int i)
+{
+ u64 local_var;
+
+ pr_info("starting %s\n", __func__);
+
+ pr_info("%s %d local_var addr: 0x%lx\n", __func__, i,
+ (unsigned long)&local_var);
+ WRITE_ONCE(g_corrupt_ptr, &local_var);
+ do_something(0, 300);
+ //buggy: return without resetting g_corrupt_ptr
+}
+
+static int silent_corruption_unwitting(void *data)
+{
+ u64 *local_ptr;
+
+ pr_debug("starting %s\n", __func__);
+
+ do {
+ local_ptr = READ_ONCE(g_corrupt_ptr);
+ do_something(0, 300);
+ } while (!local_ptr);
+
+ local_ptr[0] = 0;
+
+ return 0;
+}
+
+static void silent_corruption_victim(int i)
+{
+ u64 local_var;
+
+ pr_debug("starting %s %dth\n", __func__, i);
+
+ /* local_var random in [0xff0000, 0x100ffff] */
+ get_random_bytes(&local_var, sizeof(local_var));
+ local_var = 0xff0000 + local_var & 0xffff;
+
+ pr_debug("%s local_var addr: 0x%lx\n", __func__,
+ (unsigned long)&local_var);
+
+ do_something(0, 100);
+
+ if (local_var >= 0xff0000 && local_var <= 0xffffff)
+ pr_info("%s %d happy with 0x%llx\n", __func__, i, local_var);
+ else
+ pr_info("%s %d unhappy with 0x%llx\n", __func__, i, local_var);
+}
+
+/*
+ * Test Case 2: Silent Corruption
+ * buggy() does not protect its local var correctly
+ * unwitting() simply does its intended work
+ * victim() is unaware know what happened
+ */
+static void silent_corruption_test(void)
+{
+ struct task_struct *unwitting;
+
+ pr_info("starting %s\n", __func__);
+ WRITE_ONCE(g_corrupt_ptr, NULL);
+
+ unwitting = kthread_run(silent_corruption_unwitting, NULL, "unwitting");
+ if (IS_ERR(unwitting)) {
+ pr_err("failed to create thread2\n");
+ return;
+ }
+
+ silent_corruption_buggy(0);
+ for (int i = 0; i < 10; i++)
+ silent_corruption_victim(i);
+}
+
static ssize_t test_proc_write(struct file *file, const char __user *buffer,
size_t count, loff_t *pos)
{
@@ -88,6 +174,10 @@ static ssize_t test_proc_write(struct file *file, const char __user *buffer,
pr_info("triggering canary overflow test\n");
canary_test_overflow();
break;
+ case 2:
+ pr_info("triggering silent corruption test\n");
+ silent_corruption_test();
+ break;
default:
pr_err("Unknown test number %d\n", test_num);
return -EINVAL;
@@ -108,7 +198,8 @@ static ssize_t test_proc_read(struct file *file, char __user *buffer,
"==================================\n"
"Usage:\n"
" echo 'test0' > /proc/kstackwatch_test - Canary write test\n"
- " echo 'test1' > /proc/kstackwatch_test - Canary overflow test\n";
+ " echo 'test1' > /proc/kstackwatch_test - Canary overflow test\n"
+ " echo 'test2' > /proc/kstackwatch_test - Silent corruption test\n";
return simple_read_from_buffer(buffer, count, pos, usage,
strlen(usage));
--
2.43.0
Add a test that triggers stack writes across recursive calls,verifying
detection at specific recursion depths.
Signed-off-by: Jinchao Wang <wangjinchao600@gmail.com>
---
mm/kstackwatch/test.c | 29 ++++++++++++++++++++++++++++-
1 file changed, 28 insertions(+), 1 deletion(-)
diff --git a/mm/kstackwatch/test.c b/mm/kstackwatch/test.c
index b10465381089..6a75cd3e313d 100644
--- a/mm/kstackwatch/test.c
+++ b/mm/kstackwatch/test.c
@@ -147,6 +147,27 @@ static void silent_corruption_test(void)
silent_corruption_victim(i);
}
+/*
+ * Test Case 3: Recursive Call Corruption
+ * Test corruption detection at specified recursion depth
+ */
+static void recursive_corruption_test(int depth)
+{
+ u64 buffer[BUFFER_SIZE];
+
+ pr_info("recursive call at depth %d\n", depth);
+ pr_info("buffer 0x%lx\n", (unsigned long)buffer);
+ if (depth <= MAX_DEPTH)
+ recursive_corruption_test(depth + 1);
+
+ buffer[0] = depth;
+
+ /* make sure the compiler do not drop assign action */
+ barrier_data(buffer);
+
+ pr_info("returning from depth %d\n", depth);
+}
+
static ssize_t test_proc_write(struct file *file, const char __user *buffer,
size_t count, loff_t *pos)
{
@@ -178,6 +199,11 @@ static ssize_t test_proc_write(struct file *file, const char __user *buffer,
pr_info("triggering silent corruption test\n");
silent_corruption_test();
break;
+ case 3:
+ pr_info("triggering recursive corruption test\n");
+ /* depth start with 0 */
+ recursive_corruption_test(0);
+ break;
default:
pr_err("Unknown test number %d\n", test_num);
return -EINVAL;
@@ -199,7 +225,8 @@ static ssize_t test_proc_read(struct file *file, char __user *buffer,
"Usage:\n"
" echo 'test0' > /proc/kstackwatch_test - Canary write test\n"
" echo 'test1' > /proc/kstackwatch_test - Canary overflow test\n"
- " echo 'test2' > /proc/kstackwatch_test - Silent corruption test\n";
+ " echo 'test2' > /proc/kstackwatch_test - Silent corruption test\n"
+ " echo 'test3' > /proc/kstackwatch_test - Recursive corruption test\n";
return simple_read_from_buffer(buffer, count, pos, usage,
strlen(usage));
--
2.43.0
Provide a shell script to trigger test cases.
Signed-off-by: Jinchao Wang <wangjinchao600@gmail.com>
---
tools/kstackwatch/kstackwatch_test.sh | 40 +++++++++++++++++++++++++++
1 file changed, 40 insertions(+)
create mode 100755 tools/kstackwatch/kstackwatch_test.sh
diff --git a/tools/kstackwatch/kstackwatch_test.sh b/tools/kstackwatch/kstackwatch_test.sh
new file mode 100755
index 000000000000..61e171439ab6
--- /dev/null
+++ b/tools/kstackwatch/kstackwatch_test.sh
@@ -0,0 +1,40 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-2.0
+
+echo "IMPORTANT: Before running, make sure you have updated the offset values!"
+
+usage() {
+ echo "Usage: $0 [0-3]"
+ echo " 0 - Canary Write Test"
+ echo " 1 - Canary Overflow Test"
+ echo " 2 - Silent Corruption Test"
+ echo " 3 - Recursive Corruption Test"
+}
+
+run_test() {
+ local test_num=$1
+ case "$test_num" in
+ 0) echo "canary_test_write+0x19" >/proc/kstackwatch
+ echo "test0" >/proc/kstackwatch_test ;;
+ 1) echo "canary_test_overflow+0x1a" >/proc/kstackwatch
+ echo "test1" >/proc/kstackwatch_test ;;
+ 2) echo "silent_corruption_victim+0x32 0:8" >/proc/kstackwatch
+ echo "test2" >/proc/kstackwatch_test ;;
+ 3) echo "recursive_corruption_test+0x21+3 0:8" >/proc/kstackwatch
+ echo "test3" >/proc/kstackwatch_test ;;
+ *) usage
+ exit 1 ;;
+ esac
+ # Reset watch after test
+ echo >/proc/kstackwatch
+}
+
+# Check root and module
+[ "$EUID" -ne 0 ] && echo "Run as root" && exit 1
+for f in /proc/kstackwatch /proc/kstackwatch_test; do
+ [ ! -f "$f" ] && echo "$f not found" && exit 1
+done
+
+# Run
+[ -z "$1" ] && { usage; exit 0; }
+run_test "$1"
--
2.43.0
Add a new documentation file for KStackWatch, explaining its
purpose, motivation, key features, configuration format, module parameters,
implementation notes, limitations, and testing instructions.
Update MAINTAINERS to include Jinchao Wang as the maintainer for associated
files.
Signed-off-by: Jinchao Wang <wangjinchao600@gmail.com>
---
Documentation/dev-tools/kstackwatch.rst | 94 +++++++++++++++++++++++++
MAINTAINERS | 7 ++
2 files changed, 101 insertions(+)
create mode 100644 Documentation/dev-tools/kstackwatch.rst
diff --git a/Documentation/dev-tools/kstackwatch.rst b/Documentation/dev-tools/kstackwatch.rst
new file mode 100644
index 000000000000..f741de08ca56
--- /dev/null
+++ b/Documentation/dev-tools/kstackwatch.rst
@@ -0,0 +1,94 @@
+.. SPDX-License-Identifier: GPL-2.0
+
+====================================
+KStackWatch: Kernel Stack Watch
+====================================
+
+Overview
+========
+KStackWatch is a lightweight debugging tool designed to detect
+kernel stack corruption in real time. It helps developers capture the
+moment corruption occurs, rather than only observing a later crash.
+
+Motivation
+==========
+Stack corruption may originate in one function but manifest much later
+with no direct call trace linking the two. This makes such issues
+extremely difficult to diagnose. KStackWatch addresses this by combining
+hardware breakpoints with kprobe and fprobe instrumentation, monitoring
+stack canaries or local variables at the point of corruption.
+
+Key Features
+============
+- Lightweight overhead:
+ Minimal runtime cost, preserving bug reproducibility.
+- Real-time detection:
+ Detect stack corruption immediately.
+- Flexible configuration:
+ Control via a procfs interface.
+- Depth filtering:
+ Optional recursion depth tracking per task.
+
+Configuration
+=============
+The control file is created at::
+
+ /proc/kstackwatch
+
+To configure, write a string in the following format::
+
+ function+ip_offset[+depth] [local_var_offset:local_var_len]
+ - function : name of the target function
+ - ip_offset : instruction pointer offset within the function
+ - depth : recursion depth to watch, starting from 0
+ - local_var_offset : offset from the stack pointer at function+ip_offset
+ - local_var_len : length of the local variable(1,2,4,8)
+
+Fields
+------
+- ``function``:
+ Name of the target function to watch.
+- ``ip_offset``:
+ Instruction pointer offset within the function.
+- ``depth`` (optional):
+ Maximum recursion depth for the watch.
+- ``local_var_offset:local_var_len`` (optional):
+ A region of a local variable to monitor, relative to the stack pointer.
+ If not given, KStackWatch monitors the stack canary by default.
+
+Examples
+--------
+1. Watch the canary at the entry of ``canary_test_write``::
+
+ echo 'canary_test_write+0x12' > /proc/kstackwatch
+
+2. Watch a local variable of 8 bytes at offset 0 in
+ ``silent_corruption_victim``::
+
+ echo 'silent_corruption_victim+0x7f 0:8' > /proc/kstackwatch
+
+Module Parameters
+=================
+``panic_on_catch`` (bool)
+ - If true, trigger a kernel panic immediately on detecting stack
+ corruption.
+ - Default is false (log a message only).
+
+Implementation Notes
+====================
+- Hardware breakpoints are preallocated at watch start.
+- Function exit is monitored using ``fprobe``.
+- Per-task depth tracking is used to handle recursion across scheduling.
+- The procfs interface allows dynamic reconfiguration at runtime.
+- Active state is cleared before applying new settings.
+
+Limitations
+===========
+- Only one active watch can be configured at a time (singleton).
+- Local variable offset and size must be known in advance.
+
+Testing
+=======
+KStackWatch includes a companion test module (`kstackwatch_test`) and
+a helper script (`kstackwatch_test.sh`) to exercise different stack
+corruption scenarios:
diff --git a/MAINTAINERS b/MAINTAINERS
index cd7ff55b5d32..076512afddcc 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -13355,6 +13355,13 @@ T: git git://git.kernel.org/pub/scm/linux/kernel/git/shuah/linux-kselftest.git
F: Documentation/dev-tools/kselftest*
F: tools/testing/selftests/
+KERNEL STACK WATCH
+M: Jinchao Wang <wangjinchao600@gmail.com>
+S: Maintained
+F: Documentation/dev-tools/kstackwatch.rst
+F: mm/kstackwatch/
+F: tools/kstackwatch/
+
KERNEL SMB3 SERVER (KSMBD)
M: Namjae Jeon <linkinjeon@kernel.org>
M: Namjae Jeon <linkinjeon@samba.org>
--
2.43.0
© 2016 - 2026 Red Hat, Inc.