Technical Breakdown and Root Cause Analysis
1. Relevant Code and Architecture Context
When unwinding a kernel stack on RISC-V with frame pointers enabled (CONFIG_FRAME_POINTER), arch_stack_walk() invokes walk_stackframe() (arch/riscv/kernel/stacktrace.c).
When inspecting or dumping the stack of another task (e.g. via /proc/$PID/stack using proc_pid_stack() in fs/proc/base.c or via __get_wchan()), the target task may be running concurrently on another CPU. As functions execute and return on that target task, stack frames are allocated and reclaimed, and KASAN redzones around local stack variables are poisoned and unpoisoned. If the unwinder reads a poisoned stack slot on that task's stack, KASAN will flag the read as an out-of-bounds access.
To address this, READ_ONCE_TASK_STACK(task, x) was introduced in arch/riscv/kernel/stacktrace.c to mimic the x86 unwinder macro, intended to use READ_ONCE_NOCHECK() (which loads memory via uninstrumented __read_once_word_nocheck()) when task != current.
2. Root Cause Defect in READ_ONCE_TASK_STACK
In arch/riscv/kernel/stacktrace.c:
/*
* This disables KASAN checking when reading a value from another task's stack,
* since the other task could be running on another CPU and could have poisoned
* the stack in the meantime.
*/
#define READ_ONCE_TASK_STACK(task, x) \
({ \
unsigned long val; \
unsigned long addr = x; \
if ((task) == current) \
val = READ_ONCE(addr); \
else \
val = READ_ONCE_NOCHECK(addr); \
val; \
})
At the call sites in walk_stackframe():
fp = READ_ONCE_TASK_STACK(task, frame->fp);
pc = READ_ONCE_TASK_STACK(task, frame->ra);
The callers pass an lvalue (frame->fp or frame->ra).
In READ_ONCE_TASK_STACK, the macro initializes the local temporary:
unsigned long addr = x;
- Because
x is frame->fp, the assignment addr = x; generates a direct 8-byte load from &(frame->fp). Because walk_stackframe() is compiled with KASAN instrumentation, this direct load is checked against KASAN shadow memory. If that stack slot is marked poisoned (e.g., matching redzone markers 0xf1 or 0xf3), KASAN immediately triggers a false-positive out-of-bounds report at walk_stackframe+0x512/0x56a arch/riscv/kernel/stacktrace.c:88.
- Furthermore, passing
addr to READ_ONCE_NOCHECK(addr) expands to:
(typeof(addr))__read_once_word_nocheck(&(addr));
This takes the address of addr on the local current task's stack and reads addr without KASAN checking. The remote stack memory at &(frame->fp) was already loaded without protection during the initial assignment to addr.
3. Correct Pattern
In contrast to the buggy RISC-V implementation, the x86 implementation (arch/x86/include/asm/unwind.h) avoids any preliminary assignment:
#define READ_ONCE_TASK_STACK(task, x) \
({ \
unsigned long val; \
if (task == current) \
val = READ_ONCE(x); \
else \
val = READ_ONCE_NOCHECK(x); \
val; \
})
By passing x directly to READ_ONCE_NOCHECK(x), &(x) yields &(frame->fp), and the dereference is performed safely by __read_once_word_nocheck() with __no_sanitize_or_inline, preventing KASAN checks on remote task stacks as intended.