From 8d99aa2f9ec5780acedfff1566fb823bae65d044 Mon Sep 17 00:00:00 2001 From: Chen Pei Date: Tue, 14 Apr 2026 18:01:10 +0800 Subject: [PATCH] riscv: mm: Implement arch_within_stack_frames() for HARDENED_USERCOPY Implement arch_within_stack_frames() to enable precise per-frame stack object validation for CONFIG_HARDENED_USERCOPY on RISC-V. Per the RISC-V ELF psABI Frame Pointer Convention [1], with -fno-omit-frame-pointer (implied by CONFIG_FRAME_POINTER), the RISC-V ABI places the saved frame pointer (fp/s0) and return address (ra) at the top of each frame: high addr +------------------+ <--- fp (s0) -- frame pointer register | saved ra | fp - 1*sizeof(void*) (return address) | saved fp | fp - 2*sizeof(void*) (caller's frame pointer) +------------------+ | local variables| | spilled args | +------------------+ <--- sp low addr The allowed usercopy region within one frame is [prev_fp, fp-2*sizeof(void*)), covering local variables but excluding the saved fp/ra slots. The frame chain is walked from __builtin_frame_address(0), with prev_fp initialized to current_stack_pointer rather than the thread stack base. This ensures objects in already-returned frames are correctly detected as BAD_STACK, since no live frame will cover that region. [1] https://github.com/riscv-non-isa/riscv-elf-psabi-doc/blob/master/riscv-cc.adoc#frame-pointer-convention Signed-off-by: Chen Pei Signed-off-by: Linux RISC-V bot --- arch/riscv/Kconfig | 1 + arch/riscv/include/asm/thread_info.h | 68 ++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+) diff --git a/arch/riscv/Kconfig b/arch/riscv/Kconfig index 90c531e6abf5cf..f5e31130eb17a5 100644 --- a/arch/riscv/Kconfig +++ b/arch/riscv/Kconfig @@ -150,6 +150,7 @@ config RISCV select HAVE_ARCH_USERFAULTFD_MINOR if 64BIT && USERFAULTFD select HAVE_ARCH_USERFAULTFD_WP if 64BIT && MMU && USERFAULTFD && RISCV_ISA_SVRSW60T59B select HAVE_ARCH_VMAP_STACK if MMU && 64BIT + select HAVE_ARCH_WITHIN_STACK_FRAMES select HAVE_ASM_MODVERSIONS select HAVE_CONTEXT_TRACKING_USER select HAVE_DEBUG_KMEMLEAK diff --git a/arch/riscv/include/asm/thread_info.h b/arch/riscv/include/asm/thread_info.h index 36918c9200c92c..5899877c2a49f1 100644 --- a/arch/riscv/include/asm/thread_info.h +++ b/arch/riscv/include/asm/thread_info.h @@ -101,6 +101,74 @@ struct thread_info { void arch_release_task_struct(struct task_struct *tsk); int arch_dup_task_struct(struct task_struct *dst, struct task_struct *src); +/* + * RISC-V stack frame layout (with frame pointer enabled). + * + * Reference: RISC-V ELF psABI, Frame Pointer Convention + * https://github.com/riscv-non-isa/riscv-elf-psabi-doc/blob/master/ + * riscv-cc.adoc#frame-pointer-convention + * + * high addr + * +------------------+ <--- fp (s0) points here + * | saved ra | fp - 1*sizeof(void*) (return address) + * | saved fp | fp - 2*sizeof(void*) (previous frame pointer) + * +------------------+ + * | local vars | + * | arguments | + * +------------------+ <--- sp + * low addr + * + * The struct stackframe { fp, ra } lives at (fp - sizeof(stackframe)), + * i.e. fp[-2]=saved_fp and fp[-1]=saved_ra. + * + * For usercopy safety, we allow copies within [prev_fp, fp - 2*sizeof(void*)) + * for each frame in the chain, where prev_fp is the fp of the previous + * (lower) frame. This covers local variables and arguments but excludes + * the saved ra/fp slots at the top of the frame. + * + * We walk the frame chain starting from __builtin_frame_address(0) (the + * current frame), with prev_fp initialized to current_stack_pointer. + * Using current_stack_pointer -- rather than the 'stack' argument (which is + * the thread's entire stack base) -- ensures that objects in already-returned + * frames (address below current sp) are correctly detected as BAD_STACK, + * because no live frame in the chain will claim that region. + */ +__no_kmsan_checks +static inline int arch_within_stack_frames(const void * const stack, + const void * const stackend, + const void *obj, unsigned long len) +{ +#if defined(CONFIG_FRAME_POINTER) + const void *fp = (const void *)__builtin_frame_address(0); + const void *prev_fp = (const void *)current_stack_pointer; + + /* + * Walk the frame chain. Each iteration checks whether [obj, obj+len) + * falls within the local-variable area of the current frame: + * + * [prev_fp, fp - 2*sizeof(void*)) + * + * i.e. from the base of this frame (sp of this frame, which equals + * the fp of the frame below) up to (but not including) the saved + * fp/ra area at the top of this frame. + */ + while (stack + 2 * sizeof(void *) <= fp && fp < stackend) { + const void *frame_vars_end = (const char *)fp - 2 * sizeof(void *); + + if (obj + len <= frame_vars_end) { + if (obj >= prev_fp) + return GOOD_FRAME; + return BAD_STACK; + } + prev_fp = fp; + fp = *(const void * const *)frame_vars_end; + } + return BAD_STACK; +#else + return NOT_STACK; +#endif +} + #endif /* !__ASSEMBLER__ */ /*