All of lore.kernel.org
 help / color / mirror / Atom feed
* [PATCH v3] riscv: stacktrace: fix stack-out-of-bounds in walk_stackframe()
@ 2026-06-25 12:39 ` Jiakai Xu
  0 siblings, 0 replies; 10+ messages in thread
From: Jiakai Xu @ 2026-06-25 12:39 UTC (permalink / raw)
  To: linux-kernel, linux-riscv
  Cc: Albert Ou, Alexandre Ghiti, Chunyan Zhang, Jiakai Xu,
	Matthew Bystrin, Palmer Dabbelt, Paul Walmsley, Rui Qi,
	Samuel Holland, Jiakai Xu

The fp_is_valid() function uses ALIGN(sp, THREAD_SIZE) as the upper
bound for the frame pointer check. This bound is calculated relative
to the current sp and shifts upward when sp itself exceeds the valid
stack region, allowing the unwinder to read past the end of the
allocated task stack and triggering KASAN stack-out-of-bounds.

Fix this by using the absolute task stack boundary (task_pt_regs(task))
when sp is on the task stack. When CONFIG_IRQ_STACKS is enabled and
sp is on the IRQ stack, use the IRQ stack's top as the upper bound
instead, since task_pt_regs() points to the task stack's pt_regs
which is on a different stack and would give a wrong boundary.

This check is done once before the unwind loop, based on the initial
sp value. When unwinding starts on the IRQ stack, the IRQ stack
boundary is used for all frames, including those on the task stack
reached after crossing the call_on_irq_stack() transition. This is
safe because the IRQ stack and task stack are separate allocations;
the IRQ stack boundary will not incorrectly reject task stack frames
that are below it.

Fixes: a2a4d4a6a0bf ("riscv: stacktrace: fixed walk_stackframe()")
Signed-off-by: Jiakai Xu <jiakaiPeanut@gmail.com>
Signed-off-by: Jiakai Xu <xujiakai2025@iscas.ac.cn>
Assisted-by: YuanSheng:DeepSeek-V3.2
---
V2 -> V3:
- Handled the case where sp is on the IRQ stack (CONFIG_IRQ_STACKS), 
  as suggested by Nam Cao: when sp is not within the task stack 
  range, use the IRQ stack's top (irq_stack_ptr + IRQ_STACK_SIZE) 
  as the upper bound instead of task_pt_regs(), which would point 
  to the wrong stack. The check uses the sp variable directly rather 
  than on_thread_stack(), because on_thread_stack() reads the 
  current CPU register sp which may differ from the unwinder's sp 
  when regs is provided.
- Used IS_ENABLED(CONFIG_IRQ_STACKS) to guard the IRQ stack check,
  and cast irq_stack_ptr to unsigned long for byte-level arithmetic.
https://lore.kernel.org/linux-riscv/20260517143704.659416-1-xujiakai2025@iscas.ac.cn/T/#u

V1 -> V2:
- Moved the NULL task check from fp_is_valid() into walk_stackframe(),
  as suggested by Matthew Bystrin.
- Changed the upper bound from task_stack_page(task) + THREAD_SIZE to
  task_pt_regs(task) for a tighter boundary, as suggested by Matthew
  Bystrin.
https://lore.kernel.org/linux-riscv/20260514100711.838895-1-xujiakai2025@iscas.ac.cn/t/#u
---
 arch/riscv/kernel/stacktrace.c | 29 +++++++++++++++++++++++------
 1 file changed, 23 insertions(+), 6 deletions(-)

diff --git a/arch/riscv/kernel/stacktrace.c b/arch/riscv/kernel/stacktrace.c
index c7555447149b..b47a8acf4759 100644
--- a/arch/riscv/kernel/stacktrace.c
+++ b/arch/riscv/kernel/stacktrace.c
@@ -35,12 +35,16 @@
 extern asmlinkage void handle_exception(void);
 extern unsigned long ret_from_exception_end;
 
-static inline int fp_is_valid(unsigned long fp, unsigned long sp)
+#ifdef CONFIG_IRQ_STACKS
+DECLARE_PER_CPU(ulong *, irq_stack_ptr);
+#endif
+
+static inline int fp_is_valid(unsigned long fp, unsigned long sp,
+			      unsigned long high)
 {
-	unsigned long low, high;
+	unsigned long low;
 
 	low = sp + sizeof(struct stackframe);
-	high = ALIGN(sp, THREAD_SIZE);
 
 	return !(fp < low || fp > high || fp & 0x07);
 }
@@ -48,7 +52,7 @@ static inline int fp_is_valid(unsigned long fp, unsigned long sp)
 void notrace walk_stackframe(struct task_struct *task, struct pt_regs *regs,
 			     bool (*fn)(void *, unsigned long), void *arg)
 {
-	unsigned long fp, sp, pc;
+	unsigned long fp, sp, pc, high;
 	int graph_idx = 0;
 	int level = 0;
 
@@ -68,19 +72,32 @@ void notrace walk_stackframe(struct task_struct *task, struct pt_regs *regs,
 		pc = task->thread.ra;
 	}
 
+	if (!task)
+		task = current;
+
+	if (sp >= (unsigned long)task_stack_page(task) &&
+	    sp < (unsigned long)task_stack_page(task) + THREAD_SIZE) {
+		high = (unsigned long)task_pt_regs(task);
+	} else if (IS_ENABLED(CONFIG_IRQ_STACKS)) {
+		high = (unsigned long)this_cpu_read(irq_stack_ptr) +
+		       IRQ_STACK_SIZE;
+	} else {
+		high = (unsigned long)task_pt_regs(task);
+	}
+
 	for (;;) {
 		struct stackframe *frame;
 
 		if (unlikely(!__kernel_text_address(pc) || (level++ >= 0 && !fn(arg, pc))))
 			break;
 
-		if (unlikely(!fp_is_valid(fp, sp)))
+		if (unlikely(!fp_is_valid(fp, sp, high)))
 			break;
 
 		/* Unwind stack frame */
 		frame = (struct stackframe *)fp - 1;
 		sp = fp;
-		if (regs && (regs->epc == pc) && fp_is_valid(frame->ra, sp)) {
+		if (regs && (regs->epc == pc) && fp_is_valid(frame->ra, sp, high)) {
 			/* We hit function where ra is not saved on the stack */
 			fp = frame->ra;
 			pc = regs->ra;
-- 
2.34.1


_______________________________________________
linux-riscv mailing list
linux-riscv@lists.infradead.org
http://lists.infradead.org/mailman/listinfo/linux-riscv

^ permalink raw reply related	[flat|nested] 10+ messages in thread

* [PATCH v3] riscv: stacktrace: fix stack-out-of-bounds in walk_stackframe()
@ 2026-06-25 12:39 ` Jiakai Xu
  0 siblings, 0 replies; 10+ messages in thread
From: Jiakai Xu @ 2026-06-25 12:39 UTC (permalink / raw)
  To: linux-kernel, linux-riscv
  Cc: Albert Ou, Alexandre Ghiti, Chunyan Zhang, Jiakai Xu,
	Matthew Bystrin, Palmer Dabbelt, Paul Walmsley, Rui Qi,
	Samuel Holland, Jiakai Xu

The fp_is_valid() function uses ALIGN(sp, THREAD_SIZE) as the upper
bound for the frame pointer check. This bound is calculated relative
to the current sp and shifts upward when sp itself exceeds the valid
stack region, allowing the unwinder to read past the end of the
allocated task stack and triggering KASAN stack-out-of-bounds.

Fix this by using the absolute task stack boundary (task_pt_regs(task))
when sp is on the task stack. When CONFIG_IRQ_STACKS is enabled and
sp is on the IRQ stack, use the IRQ stack's top as the upper bound
instead, since task_pt_regs() points to the task stack's pt_regs
which is on a different stack and would give a wrong boundary.

This check is done once before the unwind loop, based on the initial
sp value. When unwinding starts on the IRQ stack, the IRQ stack
boundary is used for all frames, including those on the task stack
reached after crossing the call_on_irq_stack() transition. This is
safe because the IRQ stack and task stack are separate allocations;
the IRQ stack boundary will not incorrectly reject task stack frames
that are below it.

Fixes: a2a4d4a6a0bf ("riscv: stacktrace: fixed walk_stackframe()")
Signed-off-by: Jiakai Xu <jiakaiPeanut@gmail.com>
Signed-off-by: Jiakai Xu <xujiakai2025@iscas.ac.cn>
Assisted-by: YuanSheng:DeepSeek-V3.2
---
V2 -> V3:
- Handled the case where sp is on the IRQ stack (CONFIG_IRQ_STACKS), 
  as suggested by Nam Cao: when sp is not within the task stack 
  range, use the IRQ stack's top (irq_stack_ptr + IRQ_STACK_SIZE) 
  as the upper bound instead of task_pt_regs(), which would point 
  to the wrong stack. The check uses the sp variable directly rather 
  than on_thread_stack(), because on_thread_stack() reads the 
  current CPU register sp which may differ from the unwinder's sp 
  when regs is provided.
- Used IS_ENABLED(CONFIG_IRQ_STACKS) to guard the IRQ stack check,
  and cast irq_stack_ptr to unsigned long for byte-level arithmetic.
https://lore.kernel.org/linux-riscv/20260517143704.659416-1-xujiakai2025@iscas.ac.cn/T/#u

V1 -> V2:
- Moved the NULL task check from fp_is_valid() into walk_stackframe(),
  as suggested by Matthew Bystrin.
- Changed the upper bound from task_stack_page(task) + THREAD_SIZE to
  task_pt_regs(task) for a tighter boundary, as suggested by Matthew
  Bystrin.
https://lore.kernel.org/linux-riscv/20260514100711.838895-1-xujiakai2025@iscas.ac.cn/t/#u
---
 arch/riscv/kernel/stacktrace.c | 29 +++++++++++++++++++++++------
 1 file changed, 23 insertions(+), 6 deletions(-)

diff --git a/arch/riscv/kernel/stacktrace.c b/arch/riscv/kernel/stacktrace.c
index c7555447149b..b47a8acf4759 100644
--- a/arch/riscv/kernel/stacktrace.c
+++ b/arch/riscv/kernel/stacktrace.c
@@ -35,12 +35,16 @@
 extern asmlinkage void handle_exception(void);
 extern unsigned long ret_from_exception_end;
 
-static inline int fp_is_valid(unsigned long fp, unsigned long sp)
+#ifdef CONFIG_IRQ_STACKS
+DECLARE_PER_CPU(ulong *, irq_stack_ptr);
+#endif
+
+static inline int fp_is_valid(unsigned long fp, unsigned long sp,
+			      unsigned long high)
 {
-	unsigned long low, high;
+	unsigned long low;
 
 	low = sp + sizeof(struct stackframe);
-	high = ALIGN(sp, THREAD_SIZE);
 
 	return !(fp < low || fp > high || fp & 0x07);
 }
@@ -48,7 +52,7 @@ static inline int fp_is_valid(unsigned long fp, unsigned long sp)
 void notrace walk_stackframe(struct task_struct *task, struct pt_regs *regs,
 			     bool (*fn)(void *, unsigned long), void *arg)
 {
-	unsigned long fp, sp, pc;
+	unsigned long fp, sp, pc, high;
 	int graph_idx = 0;
 	int level = 0;
 
@@ -68,19 +72,32 @@ void notrace walk_stackframe(struct task_struct *task, struct pt_regs *regs,
 		pc = task->thread.ra;
 	}
 
+	if (!task)
+		task = current;
+
+	if (sp >= (unsigned long)task_stack_page(task) &&
+	    sp < (unsigned long)task_stack_page(task) + THREAD_SIZE) {
+		high = (unsigned long)task_pt_regs(task);
+	} else if (IS_ENABLED(CONFIG_IRQ_STACKS)) {
+		high = (unsigned long)this_cpu_read(irq_stack_ptr) +
+		       IRQ_STACK_SIZE;
+	} else {
+		high = (unsigned long)task_pt_regs(task);
+	}
+
 	for (;;) {
 		struct stackframe *frame;
 
 		if (unlikely(!__kernel_text_address(pc) || (level++ >= 0 && !fn(arg, pc))))
 			break;
 
-		if (unlikely(!fp_is_valid(fp, sp)))
+		if (unlikely(!fp_is_valid(fp, sp, high)))
 			break;
 
 		/* Unwind stack frame */
 		frame = (struct stackframe *)fp - 1;
 		sp = fp;
-		if (regs && (regs->epc == pc) && fp_is_valid(frame->ra, sp)) {
+		if (regs && (regs->epc == pc) && fp_is_valid(frame->ra, sp, high)) {
 			/* We hit function where ra is not saved on the stack */
 			fp = frame->ra;
 			pc = regs->ra;
-- 
2.34.1


^ permalink raw reply related	[flat|nested] 10+ messages in thread

* Re: [PATCH v3] riscv: stacktrace: fix stack-out-of-bounds in walk_stackframe()
  2026-06-25 12:39 ` Jiakai Xu
@ 2026-06-27 11:59   ` Nam Cao
  -1 siblings, 0 replies; 10+ messages in thread
From: Nam Cao @ 2026-06-27 11:59 UTC (permalink / raw)
  To: Jiakai Xu, linux-kernel, linux-riscv
  Cc: Albert Ou, Alexandre Ghiti, Chunyan Zhang, Jiakai Xu,
	Matthew Bystrin, Palmer Dabbelt, Paul Walmsley, Rui Qi,
	Samuel Holland, Jiakai Xu

Jiakai Xu <xujiakai2025@iscas.ac.cn> writes:
> +#ifdef CONFIG_IRQ_STACKS
> +DECLARE_PER_CPU(ulong *, irq_stack_ptr);
> +#endif

Instead of this, shouldn't we
  #include <asm/irq_stack.h>
?

> +	if (sp >= (unsigned long)task_stack_page(task) &&
> +	    sp < (unsigned long)task_stack_page(task) + THREAD_SIZE) {
> +		high = (unsigned long)task_pt_regs(task);
> +	} else if (IS_ENABLED(CONFIG_IRQ_STACKS)) {
> +		high = (unsigned long)this_cpu_read(irq_stack_ptr) +
> +		       IRQ_STACK_SIZE;

I suspect this fails to build if CONFIG_IRQ_STACKS=n, which would be
resolved if we do the suggested #include above.

> +	} else {
> +		high = (unsigned long)task_pt_regs(task);

We only get to this branch if the stack pointer is broken, right? If so,
I think printing a warning and returning is more appropriate.

Nam

^ permalink raw reply	[flat|nested] 10+ messages in thread

* Re: [PATCH v3] riscv: stacktrace: fix stack-out-of-bounds in walk_stackframe()
@ 2026-06-27 11:59   ` Nam Cao
  0 siblings, 0 replies; 10+ messages in thread
From: Nam Cao @ 2026-06-27 11:59 UTC (permalink / raw)
  To: Jiakai Xu, linux-kernel, linux-riscv
  Cc: Albert Ou, Alexandre Ghiti, Chunyan Zhang, Jiakai Xu,
	Matthew Bystrin, Palmer Dabbelt, Paul Walmsley, Rui Qi,
	Samuel Holland, Jiakai Xu

Jiakai Xu <xujiakai2025@iscas.ac.cn> writes:
> +#ifdef CONFIG_IRQ_STACKS
> +DECLARE_PER_CPU(ulong *, irq_stack_ptr);
> +#endif

Instead of this, shouldn't we
  #include <asm/irq_stack.h>
?

> +	if (sp >= (unsigned long)task_stack_page(task) &&
> +	    sp < (unsigned long)task_stack_page(task) + THREAD_SIZE) {
> +		high = (unsigned long)task_pt_regs(task);
> +	} else if (IS_ENABLED(CONFIG_IRQ_STACKS)) {
> +		high = (unsigned long)this_cpu_read(irq_stack_ptr) +
> +		       IRQ_STACK_SIZE;

I suspect this fails to build if CONFIG_IRQ_STACKS=n, which would be
resolved if we do the suggested #include above.

> +	} else {
> +		high = (unsigned long)task_pt_regs(task);

We only get to this branch if the stack pointer is broken, right? If so,
I think printing a warning and returning is more appropriate.

Nam

_______________________________________________
linux-riscv mailing list
linux-riscv@lists.infradead.org
http://lists.infradead.org/mailman/listinfo/linux-riscv

^ permalink raw reply	[flat|nested] 10+ messages in thread

* Re: [PATCH v3] riscv: stacktrace: fix stack-out-of-bounds in walk_stackframe()
  2026-06-25 12:39 ` Jiakai Xu
@ 2026-06-28 19:02   ` XIAO WU
  -1 siblings, 0 replies; 10+ messages in thread
From: XIAO WU @ 2026-06-28 19:02 UTC (permalink / raw)
  To: Jiakai Xu, linux-kernel, linux-riscv
  Cc: Albert Ou, Alexandre Ghiti, Chunyan Zhang, Matthew Bystrin,
	Palmer Dabbelt, Paul Walmsley, Rui Qi, Samuel Holland, Jiakai Xu

Hi Jiakai,

I came across the Sashiko AI review of this patch and was able to
reproduce the overflow_stack OOB issue it flagged — a KASAN
stack-out-of-bounds in walk_stackframe() on riscv64 QEMU.

The Sashiko review is at:
https://sashiko.dev/#/patchset/20260625123906.211981-1-xujiakai2025@iscas.ac.cn

 > @@ -52,19 +56,32 @@ void notrace walk_stackframe(struct task_struct 
*task, struct pt_regs *regs,
 >          pc = task->thread.ra;
 >      }
 >
 > +    if (!task)
 > +        task = current;
 > +
 > +    if (sp >= (unsigned long)task_stack_page(task) &&
 > +        sp < (unsigned long)task_stack_page(task) + THREAD_SIZE) {
 > +        high = (unsigned long)task_pt_regs(task);
 > +    } else if (IS_ENABLED(CONFIG_IRQ_STACKS)) {
 > +        high = (unsigned long)this_cpu_read(irq_stack_ptr) +
 > +               IRQ_STACK_SIZE;
 > +    } else {
 > +        high = (unsigned long)task_pt_regs(task);
 > +    }

When the stack pointer falls outside the task stack — for example when
unwinding from the overflow_stack during a stack overflow panic, or
when reading /proc/<pid>/stack for a remote task whose saved SP
happens to be on the overflow_stack — this code falls through to the
`IS_ENABLED(CONFIG_IRQ_STACKS)` branch and uses the IRQ stack boundary
as the upper limit.

The IRQ stack is 32KB (IRQ_STACK_SIZE) while the overflow_stack is
only 4KB (OVERFLOW_STACK_SIZE).  Using the IRQ stack boundary when the
SP is actually on the overflow_stack lets the unwinder read well past
the end of the valid overflow_stack allocation, triggering a KASAN
stack-out-of-bounds.

=== Reproduction ===

Kernel: 6.17.0-rc3-gab53ade87879 #1 PREEMPT
Arch:   riscv64 (qemu-system-riscv64, riscv-virtio,qemu DT)
Config: CONFIG_KASAN=y

Trigger: read /proc/<pid>/stack for a remote task (e.g. via ptrace).

=== Crash Log ===

[  862.045445][ T5937] BUG: KASAN: stack-out-of-bounds in 
walk_stackframe+0x5c4/0x66e
[  862.046336][ T5937] Read of size 8 at addr ff200000076676c8 by task 
poc/5937
[  862.048426][ T5937] CPU: 1 UID: 0 PID: 5937 Comm: poc Not tainted 
6.17.0-rc3-gab53ade87879 #1 PREEMPT
[  862.048684][ T5937] Hardware name: riscv-virtio,qemu (DT)
[  862.048684][ T5937] Call Trace:
[  862.049044][ T5937] [<ffffffff8008d882>] dump_backtrace+0x2e/0x3c
[  862.049257][ T5937] [<ffffffff8000326e>] show_stack+0x30/0x3c
[  862.049427][ T5937] [<ffffffff80074b88>] dump_stack_lvl+0x11a/0x1b0
[  862.049628][ T5937] [<ffffffff8000ee60>] print_report+0x29a/0x5b8
[  862.049838][ T5937] [<ffffffff80b0f9d8>] kasan_report+0xf0/0x21c
[  862.050031][ T5937] [<ffffffff80b1199a>] 
__asan_report_load8_noabort+0x12/0x1a
[  862.050267][ T5937] [<ffffffff8008d300>] walk_stackframe+0x5c4/0x66e
[  862.050432][ T5937] [<ffffffff86c65936>] arch_stack_walk+0x1c/0x26
[  862.050644][ T5937] [<ffffffff803e2f88>] stack_trace_save_tsk+0x164/0x1f2
[  862.050867][ T5937] [<ffffffff80e58e28>] proc_pid_stack+0x170/0x282
[  862.051032][ T5937] [<ffffffff80e5adda>] proc_single_show+0xea/0x1ca
[  862.051205][ T5937] [<ffffffff80cbea02>] seq_read_iter+0x438/0x1098
[  862.051585][ T5937] [<ffffffff80c05982>] vfs_read+0x2f0/0xaf8
[  862.052052][ T5937] [<ffffffff80c08184>] ksys_read+0x126/0x238
[  862.052550][ T5937] [<ffffffff86c8deb0>] handle_exception+0x150/0x15c
[  862.053115][ T5937] The buggy address belongs to a 8-page vmalloc region
[  862.093883][ T5937] 
==================================================================

=== PoC ===

Build:  riscv64-linux-gnu-gcc -o poc poc.c -static
Run:    ./poc    (on riscv64)

// SPDX-License-Identifier: GPL-2.0-only
// PoC: Trigger KASAN stack-out-of-bounds in walk_stackframe() on RISC-V

#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <fcntl.h>
#include <sys/mount.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <sys/syscall.h>
#include <sys/ptrace.h>
#include <sched.h>
#include <signal.h>
#include <time.h>
#include <linux/perf_event.h>

static const char *prog;

static void fail(const char *msg)
{
     fprintf(stderr, "[FAIL] %s: %s: %m\n", msg, prog);
     exit(1);
}

static void info(const char *msg)
{
     fprintf(stderr, "[INFO] %s\n", msg);
}

static void check_dmesg(const char *label)
{
     char line[4096];
     FILE *f;
     int found = 0;
     f = popen("dmesg 2>/dev/null | tail -200", "r");
     if (!f) return;
     while (fgets(line, sizeof(line), f)) {
         if (strstr(line, "KASAN") ||
             strstr(line, "stack-out-of-bounds") ||
             strstr(line, "walk_stackframe") ||
             strstr(line, "out-of-bounds") ||
             strstr(line, "Oops") ||
             strstr(line, "kernel BUG") ||
             strstr(line, "Kernel panic") ||
             strstr(line, "Call Trace")) {
             if (!found) {
                 fprintf(stderr, "\n=== Dmesg output after %s ===\n", 
label);
                 found = 1;
             }
             fprintf(stderr, "%s", line);
         }
     }
     pclose(f);
     if (found) fprintf(stderr, "=== End dmesg ===\n\n");
}

/* Approach: self stack trace with deep directory recursion */
static void try_self_stack_trace(void)
{
     pid_t pid = fork();
     if (pid < 0) return;
     if (pid == 0) {
         char dir[256];
         int i;
         getcwd(dir, sizeof(dir));
         for (i = 0; i < 500; i++) {
             char subdir[300];
             snprintf(subdir, sizeof(subdir), "%s/d%d", dir, i);
             mkdir(subdir, 0755);
             chdir(subdir);
         }
         for (i = 0; i < 1000; i++) {
             FILE *f = fopen("/proc/self/stack", "r");
             if (f) {
                 char buf[4096];
                 while (fgets(buf, sizeof(buf), f)) { }
                 fclose(f);
             }
             char path[64];
             snprintf(path, sizeof(path), "/proc/%d/stack", getpid());
             f = fopen(path, "r");
             if (f) {
                 char buf[4096];
                 while (fgets(buf, sizeof(buf), f)) { }
                 fclose(f);
             }
         }
         _exit(0);
     }
     waitpid(pid, NULL, 0);
     check_dmesg("self_stack_trace");
}

/* Approach: remote task stack trace via ptrace */
static void try_remote_task(void)
{
     pid_t child = fork();
     int status, i;
     if (child < 0) return;
     if (child == 0) {
         if (ptrace(PTRACE_TRACEME, 0, NULL, NULL) < 0)
             _exit(1);
         raise(SIGSTOP);
         struct timespec ts = { .tv_sec = 1, .tv_nsec = 0 };
         nanosleep(&ts, NULL);
         _exit(0);
     }
     waitpid(child, &status, 0);
     if (!WIFSTOPPED(status)) waitpid(child, &status, 0);
     for (i = 0; i < 100; i++) {
         char path[64];
         FILE *f;
         snprintf(path, sizeof(path), "/proc/%d/stack", child);
         f = fopen(path, "r");
         if (f) {
             char buf[4096];
             while (fgets(buf, sizeof(buf), f)) { }
             fclose(f);
         }
     }
     ptrace(PTRACE_CONT, child, NULL, NULL);
     waitpid(child, &status, 0);
     check_dmesg("remote_task");
}

int main(int argc, char **argv)
{
     prog = argv[0];
     info("=== RISC-V walk_stackframe() stack OOB PoC ===");
     info("Bug: overflow_stack (4KB) unwinding uses IRQ stack bound 
(32KB)");
     info("");
     system("dmesg -c > /dev/null 2>&1 || true");
     try_self_stack_trace();
     try_remote_task();
     info("Final dmesg check...");
     check_dmesg("final");
     info("PoC completed.");
     return 0;
}

Thanks,
Xiao


^ permalink raw reply	[flat|nested] 10+ messages in thread

* Re: [PATCH v3] riscv: stacktrace: fix stack-out-of-bounds in walk_stackframe()
@ 2026-06-28 19:02   ` XIAO WU
  0 siblings, 0 replies; 10+ messages in thread
From: XIAO WU @ 2026-06-28 19:02 UTC (permalink / raw)
  To: Jiakai Xu, linux-kernel, linux-riscv
  Cc: Albert Ou, Alexandre Ghiti, Chunyan Zhang, Matthew Bystrin,
	Palmer Dabbelt, Paul Walmsley, Rui Qi, Samuel Holland, Jiakai Xu

Hi Jiakai,

I came across the Sashiko AI review of this patch and was able to
reproduce the overflow_stack OOB issue it flagged — a KASAN
stack-out-of-bounds in walk_stackframe() on riscv64 QEMU.

The Sashiko review is at:
https://sashiko.dev/#/patchset/20260625123906.211981-1-xujiakai2025@iscas.ac.cn

 > @@ -52,19 +56,32 @@ void notrace walk_stackframe(struct task_struct 
*task, struct pt_regs *regs,
 >          pc = task->thread.ra;
 >      }
 >
 > +    if (!task)
 > +        task = current;
 > +
 > +    if (sp >= (unsigned long)task_stack_page(task) &&
 > +        sp < (unsigned long)task_stack_page(task) + THREAD_SIZE) {
 > +        high = (unsigned long)task_pt_regs(task);
 > +    } else if (IS_ENABLED(CONFIG_IRQ_STACKS)) {
 > +        high = (unsigned long)this_cpu_read(irq_stack_ptr) +
 > +               IRQ_STACK_SIZE;
 > +    } else {
 > +        high = (unsigned long)task_pt_regs(task);
 > +    }

When the stack pointer falls outside the task stack — for example when
unwinding from the overflow_stack during a stack overflow panic, or
when reading /proc/<pid>/stack for a remote task whose saved SP
happens to be on the overflow_stack — this code falls through to the
`IS_ENABLED(CONFIG_IRQ_STACKS)` branch and uses the IRQ stack boundary
as the upper limit.

The IRQ stack is 32KB (IRQ_STACK_SIZE) while the overflow_stack is
only 4KB (OVERFLOW_STACK_SIZE).  Using the IRQ stack boundary when the
SP is actually on the overflow_stack lets the unwinder read well past
the end of the valid overflow_stack allocation, triggering a KASAN
stack-out-of-bounds.

=== Reproduction ===

Kernel: 6.17.0-rc3-gab53ade87879 #1 PREEMPT
Arch:   riscv64 (qemu-system-riscv64, riscv-virtio,qemu DT)
Config: CONFIG_KASAN=y

Trigger: read /proc/<pid>/stack for a remote task (e.g. via ptrace).

=== Crash Log ===

[  862.045445][ T5937] BUG: KASAN: stack-out-of-bounds in 
walk_stackframe+0x5c4/0x66e
[  862.046336][ T5937] Read of size 8 at addr ff200000076676c8 by task 
poc/5937
[  862.048426][ T5937] CPU: 1 UID: 0 PID: 5937 Comm: poc Not tainted 
6.17.0-rc3-gab53ade87879 #1 PREEMPT
[  862.048684][ T5937] Hardware name: riscv-virtio,qemu (DT)
[  862.048684][ T5937] Call Trace:
[  862.049044][ T5937] [<ffffffff8008d882>] dump_backtrace+0x2e/0x3c
[  862.049257][ T5937] [<ffffffff8000326e>] show_stack+0x30/0x3c
[  862.049427][ T5937] [<ffffffff80074b88>] dump_stack_lvl+0x11a/0x1b0
[  862.049628][ T5937] [<ffffffff8000ee60>] print_report+0x29a/0x5b8
[  862.049838][ T5937] [<ffffffff80b0f9d8>] kasan_report+0xf0/0x21c
[  862.050031][ T5937] [<ffffffff80b1199a>] 
__asan_report_load8_noabort+0x12/0x1a
[  862.050267][ T5937] [<ffffffff8008d300>] walk_stackframe+0x5c4/0x66e
[  862.050432][ T5937] [<ffffffff86c65936>] arch_stack_walk+0x1c/0x26
[  862.050644][ T5937] [<ffffffff803e2f88>] stack_trace_save_tsk+0x164/0x1f2
[  862.050867][ T5937] [<ffffffff80e58e28>] proc_pid_stack+0x170/0x282
[  862.051032][ T5937] [<ffffffff80e5adda>] proc_single_show+0xea/0x1ca
[  862.051205][ T5937] [<ffffffff80cbea02>] seq_read_iter+0x438/0x1098
[  862.051585][ T5937] [<ffffffff80c05982>] vfs_read+0x2f0/0xaf8
[  862.052052][ T5937] [<ffffffff80c08184>] ksys_read+0x126/0x238
[  862.052550][ T5937] [<ffffffff86c8deb0>] handle_exception+0x150/0x15c
[  862.053115][ T5937] The buggy address belongs to a 8-page vmalloc region
[  862.093883][ T5937] 
==================================================================

=== PoC ===

Build:  riscv64-linux-gnu-gcc -o poc poc.c -static
Run:    ./poc    (on riscv64)

// SPDX-License-Identifier: GPL-2.0-only
// PoC: Trigger KASAN stack-out-of-bounds in walk_stackframe() on RISC-V

#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <fcntl.h>
#include <sys/mount.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <sys/syscall.h>
#include <sys/ptrace.h>
#include <sched.h>
#include <signal.h>
#include <time.h>
#include <linux/perf_event.h>

static const char *prog;

static void fail(const char *msg)
{
     fprintf(stderr, "[FAIL] %s: %s: %m\n", msg, prog);
     exit(1);
}

static void info(const char *msg)
{
     fprintf(stderr, "[INFO] %s\n", msg);
}

static void check_dmesg(const char *label)
{
     char line[4096];
     FILE *f;
     int found = 0;
     f = popen("dmesg 2>/dev/null | tail -200", "r");
     if (!f) return;
     while (fgets(line, sizeof(line), f)) {
         if (strstr(line, "KASAN") ||
             strstr(line, "stack-out-of-bounds") ||
             strstr(line, "walk_stackframe") ||
             strstr(line, "out-of-bounds") ||
             strstr(line, "Oops") ||
             strstr(line, "kernel BUG") ||
             strstr(line, "Kernel panic") ||
             strstr(line, "Call Trace")) {
             if (!found) {
                 fprintf(stderr, "\n=== Dmesg output after %s ===\n", 
label);
                 found = 1;
             }
             fprintf(stderr, "%s", line);
         }
     }
     pclose(f);
     if (found) fprintf(stderr, "=== End dmesg ===\n\n");
}

/* Approach: self stack trace with deep directory recursion */
static void try_self_stack_trace(void)
{
     pid_t pid = fork();
     if (pid < 0) return;
     if (pid == 0) {
         char dir[256];
         int i;
         getcwd(dir, sizeof(dir));
         for (i = 0; i < 500; i++) {
             char subdir[300];
             snprintf(subdir, sizeof(subdir), "%s/d%d", dir, i);
             mkdir(subdir, 0755);
             chdir(subdir);
         }
         for (i = 0; i < 1000; i++) {
             FILE *f = fopen("/proc/self/stack", "r");
             if (f) {
                 char buf[4096];
                 while (fgets(buf, sizeof(buf), f)) { }
                 fclose(f);
             }
             char path[64];
             snprintf(path, sizeof(path), "/proc/%d/stack", getpid());
             f = fopen(path, "r");
             if (f) {
                 char buf[4096];
                 while (fgets(buf, sizeof(buf), f)) { }
                 fclose(f);
             }
         }
         _exit(0);
     }
     waitpid(pid, NULL, 0);
     check_dmesg("self_stack_trace");
}

/* Approach: remote task stack trace via ptrace */
static void try_remote_task(void)
{
     pid_t child = fork();
     int status, i;
     if (child < 0) return;
     if (child == 0) {
         if (ptrace(PTRACE_TRACEME, 0, NULL, NULL) < 0)
             _exit(1);
         raise(SIGSTOP);
         struct timespec ts = { .tv_sec = 1, .tv_nsec = 0 };
         nanosleep(&ts, NULL);
         _exit(0);
     }
     waitpid(child, &status, 0);
     if (!WIFSTOPPED(status)) waitpid(child, &status, 0);
     for (i = 0; i < 100; i++) {
         char path[64];
         FILE *f;
         snprintf(path, sizeof(path), "/proc/%d/stack", child);
         f = fopen(path, "r");
         if (f) {
             char buf[4096];
             while (fgets(buf, sizeof(buf), f)) { }
             fclose(f);
         }
     }
     ptrace(PTRACE_CONT, child, NULL, NULL);
     waitpid(child, &status, 0);
     check_dmesg("remote_task");
}

int main(int argc, char **argv)
{
     prog = argv[0];
     info("=== RISC-V walk_stackframe() stack OOB PoC ===");
     info("Bug: overflow_stack (4KB) unwinding uses IRQ stack bound 
(32KB)");
     info("");
     system("dmesg -c > /dev/null 2>&1 || true");
     try_self_stack_trace();
     try_remote_task();
     info("Final dmesg check...");
     check_dmesg("final");
     info("PoC completed.");
     return 0;
}

Thanks,
Xiao


_______________________________________________
linux-riscv mailing list
linux-riscv@lists.infradead.org
http://lists.infradead.org/mailman/listinfo/linux-riscv

^ permalink raw reply	[flat|nested] 10+ messages in thread

* Re: [PATCH v3] riscv: stacktrace: fix stack-out-of-bounds in walk_stackframe()
  2026-06-27 11:59   ` Nam Cao
@ 2026-06-30  8:45     ` Jiakai Xu
  -1 siblings, 0 replies; 10+ messages in thread
From: Jiakai Xu @ 2026-06-30  8:45 UTC (permalink / raw)
  To: namcao
  Cc: alex, aou, dev.mbstr, jiakaiPeanut, linux-kernel, linux-riscv,
	palmer, pjw, qirui.001, samuel.holland, xujiakai2025,
	zhangchunyan

> Jiakai Xu <xujiakai2025@iscas.ac.cn> writes:
> > +#ifdef CONFIG_IRQ_STACKS
> > +DECLARE_PER_CPU(ulong *, irq_stack_ptr);
> > +#endif
> 
> Instead of this, shouldn't we
>   #include <asm/irq_stack.h>
> ?
> 
> > +	if (sp >= (unsigned long)task_stack_page(task) &&
> > +	    sp < (unsigned long)task_stack_page(task) + THREAD_SIZE) {
> > +		high = (unsigned long)task_pt_regs(task);
> > +	} else if (IS_ENABLED(CONFIG_IRQ_STACKS)) {
> > +		high = (unsigned long)this_cpu_read(irq_stack_ptr) +
> > +		       IRQ_STACK_SIZE;
> 
> I suspect this fails to build if CONFIG_IRQ_STACKS=n, which would be
> resolved if we do the suggested #include above.
> 
> > +	} else {
> > +		high = (unsigned long)task_pt_regs(task);
> 
> We only get to this branch if the stack pointer is broken, right? If so,
> I think printing a warning and returning is more appropriate.

Thanks for the review!  All three points make sense. I'll switch to
#include <asm/irq_stack.h>, and change the fallback else branch to
warn and return instead of silently using task_pt_regs().  Will send
a v4 with these fixed.

Jiakai


_______________________________________________
linux-riscv mailing list
linux-riscv@lists.infradead.org
http://lists.infradead.org/mailman/listinfo/linux-riscv

^ permalink raw reply	[flat|nested] 10+ messages in thread

* Re: [PATCH v3] riscv: stacktrace: fix stack-out-of-bounds in walk_stackframe()
@ 2026-06-30  8:45     ` Jiakai Xu
  0 siblings, 0 replies; 10+ messages in thread
From: Jiakai Xu @ 2026-06-30  8:45 UTC (permalink / raw)
  To: namcao
  Cc: alex, aou, dev.mbstr, jiakaiPeanut, linux-kernel, linux-riscv,
	palmer, pjw, qirui.001, samuel.holland, xujiakai2025,
	zhangchunyan

> Jiakai Xu <xujiakai2025@iscas.ac.cn> writes:
> > +#ifdef CONFIG_IRQ_STACKS
> > +DECLARE_PER_CPU(ulong *, irq_stack_ptr);
> > +#endif
> 
> Instead of this, shouldn't we
>   #include <asm/irq_stack.h>
> ?
> 
> > +	if (sp >= (unsigned long)task_stack_page(task) &&
> > +	    sp < (unsigned long)task_stack_page(task) + THREAD_SIZE) {
> > +		high = (unsigned long)task_pt_regs(task);
> > +	} else if (IS_ENABLED(CONFIG_IRQ_STACKS)) {
> > +		high = (unsigned long)this_cpu_read(irq_stack_ptr) +
> > +		       IRQ_STACK_SIZE;
> 
> I suspect this fails to build if CONFIG_IRQ_STACKS=n, which would be
> resolved if we do the suggested #include above.
> 
> > +	} else {
> > +		high = (unsigned long)task_pt_regs(task);
> 
> We only get to this branch if the stack pointer is broken, right? If so,
> I think printing a warning and returning is more appropriate.

Thanks for the review!  All three points make sense. I'll switch to
#include <asm/irq_stack.h>, and change the fallback else branch to
warn and return instead of silently using task_pt_regs().  Will send
a v4 with these fixed.

Jiakai


^ permalink raw reply	[flat|nested] 10+ messages in thread

* Re: [PATCH v3] riscv: stacktrace: fix stack-out-of-bounds in walk_stackframe()
  2026-06-28 19:02   ` XIAO WU
@ 2026-06-30  8:50     ` Jiakai Xu
  -1 siblings, 0 replies; 10+ messages in thread
From: Jiakai Xu @ 2026-06-30  8:50 UTC (permalink / raw)
  To: xiaowu.417
  Cc: alex, aou, dev.mbstr, jiakaiPeanut, linux-kernel, linux-riscv,
	palmer, pjw, qirui.001, samuel.holland, xujiakai2025,
	zhangchunyan

> Hi Jiakai,
>
> I came across the Sashiko AI review of this patch and was able to
> reproduce the overflow_stack OOB issue it flagged — a KASAN
> stack-out-of-bounds in walk_stackframe() on riscv64 QEMU.

Hi Xiao,

Thanks for the testing and the PoC.

> When the stack pointer falls outside the task stack — for example when
> unwinding from the overflow_stack during a stack overflow panic, or
> when reading /proc/<pid>/stack for a remote task whose saved SP
> happens to be on the overflow_stack — this code falls through to the
> `IS_ENABLED(CONFIG_IRQ_STACKS)` branch and uses the IRQ stack boundary
> as the upper limit.
>
> The IRQ stack is 32KB (IRQ_STACK_SIZE) while the overflow_stack is
> only 4KB (OVERFLOW_STACK_SIZE).  Using the IRQ stack boundary when the
> SP is actually on the overflow_stack lets the unwinder read well past
> the end of the valid overflow_stack allocation, triggering a KASAN
> stack-out-of-bounds.

Good point.  I'll add a dedicated overflow_stack check with the
correct 4 KiB boundary.

Jiakai


^ permalink raw reply	[flat|nested] 10+ messages in thread

* Re: [PATCH v3] riscv: stacktrace: fix stack-out-of-bounds in walk_stackframe()
@ 2026-06-30  8:50     ` Jiakai Xu
  0 siblings, 0 replies; 10+ messages in thread
From: Jiakai Xu @ 2026-06-30  8:50 UTC (permalink / raw)
  To: xiaowu.417
  Cc: alex, aou, dev.mbstr, jiakaiPeanut, linux-kernel, linux-riscv,
	palmer, pjw, qirui.001, samuel.holland, xujiakai2025,
	zhangchunyan

> Hi Jiakai,
>
> I came across the Sashiko AI review of this patch and was able to
> reproduce the overflow_stack OOB issue it flagged — a KASAN
> stack-out-of-bounds in walk_stackframe() on riscv64 QEMU.

Hi Xiao,

Thanks for the testing and the PoC.

> When the stack pointer falls outside the task stack — for example when
> unwinding from the overflow_stack during a stack overflow panic, or
> when reading /proc/<pid>/stack for a remote task whose saved SP
> happens to be on the overflow_stack — this code falls through to the
> `IS_ENABLED(CONFIG_IRQ_STACKS)` branch and uses the IRQ stack boundary
> as the upper limit.
>
> The IRQ stack is 32KB (IRQ_STACK_SIZE) while the overflow_stack is
> only 4KB (OVERFLOW_STACK_SIZE).  Using the IRQ stack boundary when the
> SP is actually on the overflow_stack lets the unwinder read well past
> the end of the valid overflow_stack allocation, triggering a KASAN
> stack-out-of-bounds.

Good point.  I'll add a dedicated overflow_stack check with the
correct 4 KiB boundary.

Jiakai


_______________________________________________
linux-riscv mailing list
linux-riscv@lists.infradead.org
http://lists.infradead.org/mailman/listinfo/linux-riscv

^ permalink raw reply	[flat|nested] 10+ messages in thread

end of thread, other threads:[~2026-06-30  8:50 UTC | newest]

Thread overview: 10+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-06-25 12:39 [PATCH v3] riscv: stacktrace: fix stack-out-of-bounds in walk_stackframe() Jiakai Xu
2026-06-25 12:39 ` Jiakai Xu
2026-06-27 11:59 ` Nam Cao
2026-06-27 11:59   ` Nam Cao
2026-06-30  8:45   ` Jiakai Xu
2026-06-30  8:45     ` Jiakai Xu
2026-06-28 19:02 ` XIAO WU
2026-06-28 19:02   ` XIAO WU
2026-06-30  8:50   ` Jiakai Xu
2026-06-30  8:50     ` Jiakai Xu

This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.