From 27547e06fd5023ba19bae204329e57b239915f81 Mon Sep 17 00:00:00 2001 From: Marco Casaroli Date: Mon, 10 Aug 2026 09:36:56 +0200 Subject: [PATCH] arch/xtensa: Provide vfork(). Xtensa selected neither fork primitive, so vfork() was simply absent. This wires it onto the two-primitive semantics. There is no assembly entry point and none is needed. Every exception entry already runs SPILL_ALL_WINDOWS, so the whole context of the calling thread is in its exception frame and copying its stack copies a complete frame chain. A flat build reaches that frame through SYS_save_context, issued inline so that the recorded stack pointer belongs to a frame that stays alive for the whole operation; a build with syscalls reaches it through xcp.sregs, recorded by xtensa_swint() for the duration of the call. The stack copy needs more than a relocated stack pointer here. A windowed ABI stores each frame's caller stack pointer absolutely, in the base save area below the frame, so a copy taken at a different address still names the parent throughout and the child's first retw would underflow onto the parent's stack. xtensa_fork_rebase() walks that chain and adds the relocation offset to each link. The copy also starts one base save area below the stack pointer rather than at it, because the frame the child resumes into keeps its caller's spilled a0-a3 there. Ported from the per-architecture work, reduced to the two primitives. Assisted-by: Claude Opus 5 (1M context) Signed-off-by: Marco Casaroli --- arch/Kconfig | 1 + arch/xtensa/include/irq.h | 10 + arch/xtensa/src/common/Make.defs | 5 + arch/xtensa/src/common/xtensa_fork.c | 455 +++++++++++++++++++ arch/xtensa/src/common/xtensa_initialstate.c | 33 +- arch/xtensa/src/common/xtensa_swint.c | 9 + 6 files changed, 504 insertions(+), 9 deletions(-) create mode 100644 arch/xtensa/src/common/xtensa_fork.c diff --git a/arch/Kconfig b/arch/Kconfig index 60039f1991e4e..e5df9c22a3393 100644 --- a/arch/Kconfig +++ b/arch/Kconfig @@ -158,6 +158,7 @@ config ARCH_X86_64 config ARCH_XTENSA bool "Xtensa" select ARCH_HAVE_BACKTRACE + select ARCH_HAVE_VFORK select ARCH_HAVE_CPUINFO select ARCH_HAVE_INTERRUPTSTACK select ARCH_HAVE_STACKCHECK diff --git a/arch/xtensa/include/irq.h b/arch/xtensa/include/irq.h index 537262ebae3ae..5b2ee5551dae2 100644 --- a/arch/xtensa/include/irq.h +++ b/arch/xtensa/include/irq.h @@ -217,6 +217,16 @@ struct xcptcontext uint32_t *regs; #ifdef CONFIG_LIB_SYSCALL + /* The exception frame of the system call currently in progress, i.e. the + * caller's register context as the vector saved it. A system call body + * runs as ordinary C code long after the exception has been dispatched, so + * this is the only way for one to reach the registers of the thread that + * made the call -- vfork() needs the caller's stack pointer and return + * address to give the child a copy. + */ + + uint32_t *sregs; + /* The following array holds the return address and the exc_return value * needed to return from each nested system call. */ diff --git a/arch/xtensa/src/common/Make.defs b/arch/xtensa/src/common/Make.defs index 3e1ea13a78582..e91fc78b5e69d 100644 --- a/arch/xtensa/src/common/Make.defs +++ b/arch/xtensa/src/common/Make.defs @@ -39,6 +39,11 @@ CMN_CSRCS += xtensa_modifyreg8.c xtensa_modifyreg16.c xtensa_modifyreg32.c CMN_CSRCS += xtensa_mpu.c xtensa_nputs.c xtensa_oneshot.c xtensa_perf.c CMN_CSRCS += xtensa_releasestack.c xtensa_registerdump.c CMN_CSRCS += xtensa_swint.c xtensa_stackframe.c + +ifneq ($(CONFIG_ARCH_HAVE_FORK)$(CONFIG_ARCH_HAVE_VFORK),) +CMN_CSRCS += xtensa_fork.c +endif + CMN_CSRCS += xtensa_saveusercontext.c CMN_CSRCS += xtensa_usestack.c xtensa_tcbinfo.c diff --git a/arch/xtensa/src/common/xtensa_fork.c b/arch/xtensa/src/common/xtensa_fork.c new file mode 100644 index 0000000000000..e45ed5ba8c079 --- /dev/null +++ b/arch/xtensa/src/common/xtensa_fork.c @@ -0,0 +1,455 @@ +/**************************************************************************** + * arch/xtensa/src/common/xtensa_fork.c + * + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. The + * ASF licenses this file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + ****************************************************************************/ + +/**************************************************************************** + * Included Files + ****************************************************************************/ + +#include + +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include + +#include "sched/sched.h" +#include "xtensa.h" +#include "chip_macros.h" + +/**************************************************************************** + * Pre-processor Definitions + ****************************************************************************/ + +/* The base save area: the 16 bytes below a frame's stack pointer, holding + * the spilled a0-a3 of that frame's caller. Window overflow writes them and + * underflow reads them back, so it is the link that makes the frame chain + * walkable, and the reason a copy starting at the stack pointer is missing + * its first link. + */ + +#define BASE_SAVE_AREA 16 +#define BASE_SAVE_A1 1 /* a0, a1, a2, a3 -- a1 is the caller's SP */ + +/**************************************************************************** + * Private Types + ****************************************************************************/ + +/* Everything a child is built from: the register context of the thread that + * called, and where that thread was. There are two ways to come by it, and + * they differ in more than provenance -- see xtensa_fork_direct() below. + */ + +struct fork_snapshot_s +{ + FAR const uint32_t *regs; /* The caller's full register context */ + uintptr_t usp; /* The caller's user stack pointer */ + uintptr_t pc; /* Where the child resumes */ + uint32_t a2; /* What the child sees returned in A2 */ +#ifndef CONFIG_BUILD_FLAT + uintptr_t ctx; /* The caller's privilege, from its syscall */ +#endif +}; + +/**************************************************************************** + * Private Functions + ****************************************************************************/ + +/**************************************************************************** + * Name: xtensa_fork_rebase + * + * Description: + * Rebase the frame chain of a relocated stack copy. + * + * A windowed ABI stores each frame's caller stack pointer absolutely, in + * the base save area at [sp - 16), so a copy taken at a different address + * still names the parent and the child's first retw would underflow onto + * the parent's stack. Each link gets the relocation offset added. + * + * Spilled a4-a15 and any other stack addresses in the copy are left alone. + * Those are data; the chain is what the child needs to return at all. + * + * Input Parameters: + * newsp - The child's stack pointer + * usp - The parent's stack pointer, which newsp is a relocation of + * stacktop - The top of the parent's stack; the walk ends there + * offset - newsp - usp, the amount every link moves by + * + ****************************************************************************/ + +static void xtensa_fork_rebase(uintptr_t newsp, uintptr_t usp, + uintptr_t stacktop, intptr_t offset) +{ + uintptr_t csp = newsp; /* The frame being fixed, in the child's copy */ + uintptr_t psp = usp; /* The same frame, as the parent addresses it */ + + while (psp < stacktop) + { + FAR uint32_t *save = (FAR uint32_t *)(csp - BASE_SAVE_AREA); + uintptr_t caller = save[BASE_SAVE_A1]; + + /* The chain grows towards the top of the stack and ends there. Stop + * on anything else rather than following it: the outermost frame's + * save area was never written by an overflow, so what is in it is + * whatever the stack was coloured with. + */ + + if (caller <= psp || caller > stacktop) + { + break; + } + + save[BASE_SAVE_A1] = (uint32_t)(uintptr_t)((intptr_t)caller + offset); + + psp = caller; + csp = (uintptr_t)((intptr_t)caller + offset); + } +} + +/**************************************************************************** + * Name: xtensa_fork_stack + * + * Description: + * Give the child its stack pointer, copying the parent's frames if needed. + * + * A fork() child keeps the parent's stack addresses inside its own address + * environment, so it has nothing to copy. A vfork() child gets a stack of + * its own, which needs the copy and xtensa_fork_rebase() with it. + * + * The copy starts one base save area below the stack pointer: the frame + * the child resumes into keeps its caller's spilled a0-a3 there. + * + * Input Parameters: + * parent - The parent task's TCB + * child - The child task's TCB + * usp - The parent's stack pointer + * + * Returned Value: + * The child's stack pointer. + * + ****************************************************************************/ + +static uintptr_t xtensa_fork_stack(FAR struct tcb_s *parent, + FAR struct tcb_s *child, + uintptr_t usp) +{ + uintptr_t stacktop; + uintptr_t stackutil; + uintptr_t newtop; + uintptr_t newsp; + + stacktop = (uintptr_t)parent->stack_base_ptr + parent->adj_stack_size; + DEBUGASSERT(stacktop > usp); + + if (child->stack_base_ptr == parent->stack_base_ptr) + { + /* The child is running at the parent's stack addresses, inside its + * own duplicated address environment. There is nothing to relocate: + * every stack address the child inherits is still the address it + * names. + */ + + return usp; + } + + DEBUGASSERT(usp - BASE_SAVE_AREA >= (uintptr_t)parent->stack_base_ptr); + + stackutil = stacktop - (usp - BASE_SAVE_AREA); + newtop = (uintptr_t)child->stack_base_ptr + child->adj_stack_size; + + /* The copy has to fit, and the register save area goes below it when the + * child has no kernel stack to put it on -- see xtensa_fork(). + */ + + DEBUGASSERT(newtop - stackutil > + (uintptr_t)child->stack_base_ptr + XCPTCONTEXT_SIZE); + + newsp = newtop - stackutil + BASE_SAVE_AREA; + + memcpy((FAR void *)(newsp - BASE_SAVE_AREA), + (FAR const void *)(usp - BASE_SAVE_AREA), stackutil); + + xtensa_fork_rebase(newsp, usp, stacktop, (intptr_t)(newsp - usp)); + + return newsp; +} + +/**************************************************************************** + * Name: xtensa_fork + * + * Description: + * The common core of the two primitives. They differ in the flag handed + * to nxtask_setup_fork(), which is where the memory semantics are decided, + * and in where their snapshot of the caller comes from. + * + * Input Parameters: + * vfork - true for vfork(), false for fork() + * snap - The caller's context; see struct fork_snapshot_s + * + * Returned Value: + * The pid of the child, or ERROR on failure. + * + ****************************************************************************/ + +static pid_t xtensa_fork(bool vfork, FAR const struct fork_snapshot_s *snap) +{ + FAR struct tcb_s *parent = this_task(); + FAR struct tcb_s *child; + uintptr_t newsp; + uintptr_t regstop; + + DEBUGASSERT(snap->regs != NULL && snap->pc != 0); + + /* Allocate and initialise a TCB for the child. The start address is only + * bookkeeping here: what the child actually resumes with is the register + * context assembled below. + */ + + child = nxtask_setup_fork((start_t)snap->pc, vfork); + if (child == NULL) + { + sinfo("nxtask_setup_fork failed\n"); + return (pid_t)ERROR; + } + + newsp = xtensa_fork_stack(parent, child, snap->usp); + + /* Where the child's register context is restored from. */ + +#ifdef CONFIG_ARCH_KERNEL_STACK + if (child->xcp.kstack != NULL) + { + /* On the kernel stack: the child is resumed by the same path a system + * call returns through, which runs in kernel context. + */ + + regstop = (uintptr_t)child->xcp.ktopstk; + } + else +#endif + { + /* There is no kernel stack, so it goes on the child's own stack, below + * the base save area rather than at the stack pointer. Writing + * XCPTCONTEXT_SIZE bytes down from newsp would destroy the very words + * the child's first `retw' reads. It is dead memory once the context + * has been restored, so the child may then grow over it. + * + * This is only sound because a child without a kernel stack always has + * a stack of its own: writing here on a shared stack would land in + * the parent's frames. See xtensa_fork_stack(). + */ + + DEBUGASSERT(child->stack_base_ptr != parent->stack_base_ptr); + regstop = newsp - BASE_SAVE_AREA; + } + + child->xcp.regs = (FAR uint32_t *)(regstop - XCPTCONTEXT_SIZE); + + /* Start from the parent's context, then correct what must differ */ + + memcpy(child->xcp.regs, snap->regs, XCPTCONTEXT_SIZE); + + /* The child is not returning the way the parent will: it is being + * started. Give it directly what its resume path would have produced -- + * the address to resume at, its privilege, its own stack, and the value + * the call returns to it. + * + * The privilege matters most. The frame copied above carries the world + * the *exception* left in it, not the caller's; taking it would resume an + * unprivileged process privileged. + */ + + child->xcp.regs[REG_PC] = snap->pc; + child->xcp.regs[REG_A1] = newsp; + child->xcp.regs[REG_A2] = snap->a2; + +#ifndef CONFIG_BUILD_FLAT + xtensa_restoreprivilege(child->xcp.regs, snap->ctx); +#endif + +#ifdef CONFIG_ARCH_KERNEL_STACK + /* The parent is inside a system call, so its saved user stack pointer is + * held aside in ustkptr and A1 names the kernel stack. The child is not + * inside that call and must not inherit it. + */ + + child->xcp.ustkptr = NULL; +#endif + + /* And start the child. On failure nxtask_start_fork() discards the TCB + * through nxtask_abort_fork(). + */ + + return nxtask_start_fork(child, vfork); +} + +#ifdef CONFIG_LIB_SYSCALL +/**************************************************************************** + * Name: xtensa_fork_syscall + * + * Description: + * Fork from the system call that brought the caller into the kernel. + * + * There is no assembly counterpart to this, and it does not need one. + * Other architectures enter through a stub that spills the caller's + * registers into a struct fork_s, because a C function cannot see its + * caller's callee-saved registers. Here the system call has already done + * better than that: _xtensa_context_save() runs SPILL_ALL_WINDOWS on + * every exception entry, so every live register window of the calling + * thread has been written to its stack and the whole context is in the + * exception frame. Copying the stack therefore copies a complete and + * self-consistent frame chain. xcp.sregs is that frame (see + * xtensa_swint()). + * + * The child resumes where the system call would have returned, and A2 is + * the value the call yields -- 0, as fork() and vfork() return to a child. + * + ****************************************************************************/ + +static pid_t xtensa_fork_syscall(bool vfork) +{ + FAR struct tcb_s *parent = this_task(); + struct fork_snapshot_s snap; + int index; + + /* This runs as the body of a system call, so the caller's own state is not + * simply what is in the exception frame. + */ + + DEBUGASSERT(parent->xcp.sregs != NULL); + index = (int)parent->xcp.nsyscalls - 1; + DEBUGASSERT(index >= 0); + + snap.regs = parent->xcp.sregs; + snap.pc = parent->xcp.syscall[index].sysreturn; + snap.a2 = 0; +#ifndef CONFIG_BUILD_FLAT + snap.ctx = parent->xcp.syscall[index].int_ctx; +#endif + + /* The stack pointer to work from is the *user* one. A kernel build moves + * the outermost system call onto the thread's kernel stack and holds the + * user stack pointer aside in ustkptr, so the A1 in the exception frame + * names the kernel stack from here on -- measuring the parent's user stack + * against it would produce a nonsense length. + */ + +#ifdef CONFIG_ARCH_KERNEL_STACK + snap.usp = parent->xcp.ustkptr != NULL ? + (uintptr_t)parent->xcp.ustkptr : snap.regs[REG_A1]; +#else + snap.usp = snap.regs[REG_A1]; +#endif + + return xtensa_fork(vfork, &snap); +} + +#else /* CONFIG_LIB_SYSCALL */ + +/**************************************************************************** + * Name: xtensa_fork_direct + * + * Description: + * Fork a caller that did not arrive through a system call, which in a flat + * build is every caller. + * + * SYS_save_context spills every window and copies out the exception frame, + * giving the same snapshot a system call would have left behind. + * + * It is issued inline rather than through up_saveusercontext() because the + * snapshot records the stack pointer of the frame that issues it, and the + * child resumes on a copy of the stack from that point up. Everything + * called from here runs below this frame, so the copied region stays + * valid; a helper's frame would be dead and reused by the time the child + * ran on it. + * + * The child resumes after the syscall rather than at a syscall return, so + * the PC comes from the frame, and A2 is 1 so the branch below can tell + * parent from child. + * + * Returned Value: + * The pid of the child to the parent, 0 to the child, ERROR on failure. + * + ****************************************************************************/ + +static pid_t xtensa_fork_direct(bool vfork) +{ + uint32_t regs[XCPTCONTEXT_REGS] aligned_data(16); + struct fork_snapshot_s snap; + + if (sys_call1(SYS_save_context, (uintptr_t)regs) != 0) + { + /* The child, resumed from the context captured just above with A2 set + * to 1. It has nothing to do but leave. + */ + + return 0; + } + + snap.regs = regs; + snap.usp = regs[REG_A1]; + snap.pc = regs[REG_PC]; + snap.a2 = 1; + + return xtensa_fork(vfork, &snap); +} +#endif /* CONFIG_LIB_SYSCALL */ + +/**************************************************************************** + * Public Functions + ****************************************************************************/ + +/**************************************************************************** + * Name: up_fork + * + * Description: + * The architecture half of fork() and vfork(). With vfork true the child + * shares the parent's memory and runs on a private copy of its stack, and + * the parent is suspended until the child leaves. With vfork false the + * child receives its own copy of the parent's memory at the same virtual + * addresses. + * + * Input Parameters: + * vfork - true for vfork(), false for fork() + * + * Returned Value: + * Upon successful completion, 0 is returned to the child and the pid of + * the child to the parent. Otherwise, -1 is returned to the parent, no + * child is created, and errno is set to indicate the error. + * + ****************************************************************************/ + +pid_t up_fork(bool vfork) +{ +#ifdef CONFIG_LIB_SYSCALL + return xtensa_fork_syscall(vfork); +#else + return xtensa_fork_direct(vfork); +#endif +} diff --git a/arch/xtensa/src/common/xtensa_initialstate.c b/arch/xtensa/src/common/xtensa_initialstate.c index 1a0a14579c2e6..76549e33b1a48 100644 --- a/arch/xtensa/src/common/xtensa_initialstate.c +++ b/arch/xtensa/src/common/xtensa_initialstate.c @@ -78,7 +78,7 @@ void up_initial_state(struct tcb_s *tcb) { struct xcptcontext *xcp = &tcb->xcp; #ifdef CONFIG_SCHED_THREAD_LOCAL - const uint32_t base = ALIGN_UP((uint32_t)&_rodata_reserved_align, + const uintptr_t base = ALIGN_UP((uintptr_t)&_rodata_reserved_align, TCB_SIZE); #endif @@ -107,9 +107,24 @@ void up_initial_state(struct tcb_s *tcb) /* Initialize the context registers to stack top */ - xcp->regs = (void *)((uint32_t)tcb->stack_base_ptr + - tcb->adj_stack_size - - XCPTCONTEXT_SIZE); +#ifdef CONFIG_ARCH_KERNEL_STACK + if (xcp->kstack != NULL) + { + /* Put the frame on the thread's kernel stack rather than its user + * stack. It is restored in kernel context, and leaving it in user + * memory means the thread can scribble on the register set it is + * about to be started with. + */ + + xcp->regs = (void *)((uintptr_t)xcp->ktopstk - XCPTCONTEXT_SIZE); + } + else +#endif + { + xcp->regs = (void *)((uintptr_t)tcb->stack_base_ptr + + tcb->adj_stack_size - + XCPTCONTEXT_SIZE); + } /* Initialize the xcp registers */ @@ -117,9 +132,9 @@ void up_initial_state(struct tcb_s *tcb) /* Set initial values of registers */ - xcp->regs[REG_PC] = (uint32_t)tcb->start; /* Task entrypoint */ - xcp->regs[REG_A0] = 0; /* To terminate GDB backtrace */ - xcp->regs[REG_A1] = (uint32_t)tcb->stack_base_ptr + /* Physical top of stack frame */ + xcp->regs[REG_PC] = (uintptr_t)tcb->start; /* Task entrypoint */ + xcp->regs[REG_A0] = 0; /* To terminate GDB backtrace */ + xcp->regs[REG_A1] = (uintptr_t)tcb->stack_base_ptr + /* Physical top of stack frame */ tcb->adj_stack_size; /* Each task access the TLS variables using the THREADPTR register plus an @@ -228,8 +243,8 @@ void up_initial_state(struct tcb_s *tcb) #ifdef CONFIG_SCHED_THREAD_LOCAL xcp->regs[REG_THREADPTR] = (uintptr_t)tcb->stack_alloc_ptr + sizeof(struct tls_info_s) - - ((uint32_t)&_thread_local_start - - (uint32_t)&_rodata_reserved_start) - base; + ((uintptr_t)&_thread_local_start - + (uintptr_t)&_rodata_reserved_start) - base; #endif /* Set initial PS to int level 0, user mode. */ diff --git a/arch/xtensa/src/common/xtensa_swint.c b/arch/xtensa/src/common/xtensa_swint.c index 4b38987e17e38..035811f249260 100644 --- a/arch/xtensa/src/common/xtensa_swint.c +++ b/arch/xtensa/src/common/xtensa_swint.c @@ -66,6 +66,15 @@ int xtensa_swint(int irq, void *context, void *arg) cmd = regs[REG_A2]; +#ifdef CONFIG_LIB_SYSCALL + /* Record the caller's register context for the duration of the call. A + * system call body runs as C code after this exception has returned, so + * this is how it reaches the registers of the thread that called it. + */ + + tcb->xcp.sregs = regs; +#endif + /* The syscall software interrupt is called with A2 = system call command * and A3..A9 = variable number of arguments depending on the system call. */