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 <marco.casaroli@gmail.com>
This commit is contained in:
Marco Casaroli 2026-07-27 12:48:10 +02:00 • committed by Alan C. Assis
parent 05931b7014
commit f8c0bc77fb

View file

@ -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();