From: cp0613@linux.alibaba.com
To: wangruikang@iscas.ac.cn, pjw@kernel.org, alex@ghiti.fr,
cleger@rivosinc.com, alexghiti@rivosinc.com, guoren@kernel.org
Cc: linux-riscv@lists.infradead.org, linux-kernel@vger.kernel.org,
Chen Pei <cp0613@linux.alibaba.com>
Subject: [PATCH v2] riscv: mm: Implement arch_within_stack_frames() for HARDENED_USERCOPY
Date: Tue, 14 Apr 2026 18:01:10 +0800 [thread overview]
Message-ID: <20260414100111.99567-1-cp0613@linux.alibaba.com> (raw)
From: Chen Pei <cp0613@linux.alibaba.com>
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 <cp0613@linux.alibaba.com>
---
== Testing ==
Build requirements:
CONFIG_HARDENED_USERCOPY=y
CONFIG_FRAME_POINTER=y (already selected by default on riscv)
CONFIG_LKDTM=y
CONFIG_DEBUG_FS=y
Run on QEMU or target board as root:
# Trigger copy_to_user from a stack frame that has already returned:
echo USERCOPY_STACK_FRAME_TO > /sys/kernel/debug/provoke-crash/DIRECT
# Trigger copy_from_user into a stack frame that has already returned:
echo USERCOPY_STACK_FRAME_FROM > /sys/kernel/debug/provoke-crash/DIRECT
# Trigger copy beyond current stack bounds:
echo USERCOPY_STACK_BEYOND > /sys/kernel/debug/provoke-crash/DIRECT
== Kernel test logs ==
# echo USERCOPY_STACK_FRAME_TO > /sys/kernel/debug/provoke-crash/DIRECT
lkdtm: Performing direct entry USERCOPY_STACK_FRAME_TO
lkdtm: good_stack: ff20000000323c98-ff20000000323cb8
lkdtm: bad_stack : ff20000000323c18-ff20000000323c38
lkdtm: attempting good copy_to_user of local stack
lkdtm: attempting bad copy_to_user of distant stack
usercopy: Kernel memory exposure attempt detected from process stack (offset 38, size 20)!
......
(kernel BUG Call Trace...)
......
Changes in v2:
- Simplify commit messages.
- Drop the redundant #ifdef CONFIG_HAVE_ARCH_WITHIN_STACK_FRAMES.
- Corrected RISC-V stack frame layout description for RV32 compatibility.
- Add a reference link to the RISC-V ELF psABI Frame Pointer Convention.
- Fix potential out-of-bounds read when fp is too close to the stack bottom.
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 90c531e6abf5..f5e31130eb17 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 36918c9200c9..5899877c2a49 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__ */
/*
--
2.50.1
_______________________________________________
linux-riscv mailing list
linux-riscv@lists.infradead.org
http://lists.infradead.org/mailman/listinfo/linux-riscv
reply other threads:[~2026-04-14 10:01 UTC|newest]
Thread overview: [no followups] expand[flat|nested] mbox.gz Atom feed
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20260414100111.99567-1-cp0613@linux.alibaba.com \
--to=cp0613@linux.alibaba.com \
--cc=alex@ghiti.fr \
--cc=alexghiti@rivosinc.com \
--cc=cleger@rivosinc.com \
--cc=guoren@kernel.org \
--cc=linux-kernel@vger.kernel.org \
--cc=linux-riscv@lists.infradead.org \
--cc=pjw@kernel.org \
--cc=wangruikang@iscas.ac.cn \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox