From f8c0bc77fb8f17afb02b8e0b2a33eb89d4ea4b1b Mon Sep 17 00:00:00 2001 From: Marco Casaroli Date: Mon, 27 Jul 2026 12:48:10 +0200 Subject: [PATCH] arch/risc-v: Recover from a user fault without a kernel stack. In CONFIG_BUILD_PROTECTED, a user task that touches memory it does not own must be terminated on its own. The rest of the system must keep running. riscv_fault_handler() already does this: it checks for a fault taken from U-mode, sets TCB_FLAG_FORCED_CANCEL and changes the exception return to _exit(SIGSEGV) in privileged mode. But the whole block was inside #ifdef CONFIG_ARCH_KERNEL_STACK. Configurations that do not select that symbol, such as rv-virt:pnsh and rv-virt:pnsh64, fell through to PANIC_WITH_REGS(). A contained user-space bug stopped the whole system. Only the last line of the block needs a kernel stack: running_regs()[REG_SP] = tcb->xcp.ktopstk; because xcp.ktopstk exists only with one. Narrow the guard to that assignment, so the rest compiles in all configurations. arm64 already does the same in arm64_fatal_handler(). It is correct to leave REG_SP unchanged. In riscv_exception_common.S the switch to the kernel stack at exception entry is also inside #ifdef CONFIG_ARCH_KERNEL_STACK. Without a kernel stack, the exception frame goes on the user stack and REG_SP holds the user SP. dispatch_syscall() already runs on that stack, so the kernel runs all system calls of this task there, exit() included. Running _exit on it after a fault is the same case and adds no new exposure. The stack also stays mapped until the scheduler switches away, because a build without a kernel stack cannot select CONFIG_ARCH_ADDRENV (riscv_exception_common.S has an #error for that combination). No behaviour change in other builds. The recovery runs only when the saved STATUS_PPP is clear, that is, when the fault came from U-mode. In CONFIG_BUILD_FLAT, tasks run at kernel privilege (M-mode, or S-mode on the nsbi configurations), so STATUS_PPP is set and the panic path stays the same. CONFIG_BUILD_KERNEL configurations select ARCH_KERNEL_STACK through ARCH_ADDRENV, so their code does not change. Tested on QEMU with examples/sandbox and ostest. On rv-virt:pnsh and rv-virt:pnsh64 a forbidden read or write of kernel memory now kills only the offending task, with status 2816 (SIGSEGV). The shell and an unrelated thread keep running. Before this change the same access caused a PANIC. ostest exits with status 0 on both, before and after this change. rv-virt:knsh still builds, and its riscv_exception.o differs only in the __LINE__ value of the PANIC call. Assisted-by: Claude Code:claude-opus-5-5 Signed-off-by: Marco Casaroli --- arch/risc-v/src/common/riscv_exception.c | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/arch/risc-v/src/common/riscv_exception.c b/arch/risc-v/src/common/riscv_exception.c index 3d45d264996..1865af7d355 100644 --- a/arch/risc-v/src/common/riscv_exception.c +++ b/arch/risc-v/src/common/riscv_exception.c @@ -102,7 +102,6 @@ static const char *g_reasons_str[RISCV_MAX_EXCEPTION + 1] = static void riscv_fault_handler(uintreg_t cause, void *regs) { -#ifdef CONFIG_ARCH_KERNEL_STACK struct tcb_s *tcb = this_task(); /* The STATUS_PPP check alone is enough: any kernel-mode fault (kernel @@ -131,14 +130,26 @@ static void riscv_fault_handler(uintreg_t cause, void *regs) running_regs()[REG_A0] = (void *)SIGSEGV; ((uintreg_t *)running_regs())[REG_INT_CTX] |= STATUS_PPP; +#ifdef CONFIG_ARCH_KERNEL_STACK /* Continue with kernel stack in use. The frame(s) in kernel stack * are no longer needed, so just set it to top */ running_regs()[REG_SP] = tcb->xcp.ktopstk; +#endif + + /* Without a per-process kernel stack REG_SP is left alone, and _exit + * runs on the user stack the task faulted on. That is the same stack + * exception_common already pushed this frame onto, and the same one + * dispatch_syscall uses for this task's system calls, so it is no new + * exposure -- see the CONFIG_ARCH_KERNEL_STACK blocks in + * riscv_exception_common.S. Such a configuration cannot have + * CONFIG_ARCH_ADDRENV either, so the stack stays mapped until the + * scheduler switches away from the dying task. + */ + return; } -#endif _alert("PANIC!!! Exception = %" PRIxREG "\n", cause); up_irq_save();