This is an automated email from the ASF dual-hosted git repository.

acassis pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/nuttx.git

commit 608d1e269a77bf1b50582c5e1c43422f67017331
Author: Marco Casaroli <[email protected]>
AuthorDate: Sun Aug 9 14:53:32 2026 +0200

    arch/arm: Build fork() children from the caller's syscall frame on armv7-a.
    
    In a kernel build the cloning primitives are reached through a system call,
    and armv7-a dispatches one by re-pointing the caller's own exception frame 
at
    dispatch_syscall() and switching to the task's kernel stack.  The snapshot 
the
    entry point in fork.S takes for itself therefore describes the kernel-side
    stub, and the frames below it are on a stack the child gets no copy of:  a
    child built from that snapshot resumes at a kernel address with a stack
    pointer into its own user stack.  It faulted with a prefetch abort at PC 0 
on
    qemu-armv7a:knsh, which is why the fork family had never been run there.
    
    Record what the caller was actually doing instead.  arm_syscall() stores the
    exception frame of the outermost system call in xcp.sregs, mirroring
    riscv_swint.c, and arm_fork() chooses where the caller's registers live:
    
      arm_fork_syscall()  when a user stack pointer is saved, rebuilding the 
child
                          from xcp.sregs so that it returns from the very same 
SVC
                          as the parent, in the same mode, on its own stack and
                          with no inherited system call nesting;
      arm_fork_direct()   otherwise -- the flat build, a kernel thread in any
                          build, and a build without a kernel stack, where the
                          call is dispatched on the caller's own stack so the
                          caller's frames are copied along with the kernel-side
                          ones.
    
    Note that the discriminator is xcp.ustkptr rather than TCB_FLAG_SYSCALL.  On
    armv7-a the caller is the task that runs the kernel side of its own system
    call, so being in a system call is not by itself a reason to distrust the
    snapshot; the switch to the kernel stack is.  Because arm_syscall() has
    already re-pointed the frame by the time arm_fork() runs, the caller's PC,
    CPSR and SP come from where arm_syscall() put them -- syscall[0].sysreturn,
    syscall[0].cpsr and ustkptr -- and the rest from the frame itself.
    
    Nothing selects the primitives on an ARM kernel build yet, so this commit
    changes no configuration; it is what the next one needs to be correct.
    
    Assisted-by: Claude Code:claude-opus-5
    Signed-off-by: Marco Casaroli <[email protected]>
---
 Documentation/guides/fork_vfork_migration.rst |  20 ++
 arch/arm/include/armv7-a/irq.h                |  16 ++
 arch/arm/src/armv7-a/arm_syscall.c            |  14 ++
 arch/arm/src/common/arm_fork.c                | 288 +++++++++++++++++++++-----
 4 files changed, 291 insertions(+), 47 deletions(-)

diff --git a/Documentation/guides/fork_vfork_migration.rst 
b/Documentation/guides/fork_vfork_migration.rst
index 95c22f701f6..8420fbe5b7c 100644
--- a/Documentation/guides/fork_vfork_migration.rst
+++ b/Documentation/guides/fork_vfork_migration.rst
@@ -153,6 +153,26 @@ exception frame when it traps -- ``xcp.sregs`` is the 
field that exists for
 this -- and build the child from that instead, while a kernel thread that calls
 the entry point directly still takes the ordinary path.
 
+Two architectures do it, and they are worth copying:
+
+* RISC-V: ``riscv_swint.c`` stores the frame in ``xcp.sregs``, and
+  ``riscv_fork.c`` rebuilds the child from it.
+* armv7-a: ``arm_syscall.c`` stores the frame in ``xcp.sregs``, and
+  ``arm_fork()`` dispatches to ``arm_fork_syscall()`` or
+  ``arm_fork_direct()``.  The discriminator here is a saved user stack
+  pointer, ``xcp.ustkptr``, rather than ``TCB_FLAG_SYSCALL``:  armv7-a
+  dispatches a system call by re-pointing the caller's own exception frame at
+  ``dispatch_syscall()``, so the caller *is* the task that runs the kernel
+  side of the call.  What makes its snapshot useless is not the system call
+  as such but the switch to the kernel stack, which leaves the kernel-side
+  frames on a stack the child gets no copy of.  A build without a kernel
+  stack dispatches on the caller's own stack, so there the frames are copied
+  along with the caller's and ``arm_fork_direct()`` remains correct.  Because
+  ``arm_syscall()`` has already re-pointed the frame by the time
+  ``arm_fork()`` runs, its PC, CPSR and SP are the kernel's; the caller's are
+  read from where ``arm_syscall()`` put them -- ``syscall[0].sysreturn``,
+  ``syscall[0].cpsr`` and ``ustkptr``.
+
 Nothing else is required:  the ``up_fork()`` entry point and the libc wrapper
 are already there and become live automatically.
 
diff --git a/arch/arm/include/armv7-a/irq.h b/arch/arm/include/armv7-a/irq.h
index bf1a5083dcb..4c767e85caf 100644
--- a/arch/arm/include/armv7-a/irq.h
+++ b/arch/arm/include/armv7-a/irq.h
@@ -301,6 +301,22 @@ struct xcptcontext
 
   uint8_t nsyscalls;
   struct xcpt_syscall_s syscall[CONFIG_SYS_NNEST];
+
+  /* Where the register save area of the caller of the outermost system call
+   * is, which is the exception frame arm_vectorsvc built on the caller's own
+   * stack.  It is recorded by arm_syscall() and is what the cloning
+   * primitives build the child's context from:  a fork() or vfork() reached
+   * through a system call has to give the child the registers of the task
+   * that trapped, not those of the kernel-side stub that arm_fork() is
+   * called from.  See arm_fork().
+   *
+   * The frame is the one arm_syscall() has already re-pointed at
+   * dispatch_syscall():  its PC, CPSR, R0 and SP are the kernel's.  The
+   * caller's own values are in syscall[0].sysreturn, syscall[0].cpsr and
+   * ustkptr respectively; R0 is the return value and belongs to neither.
+   */
+
+  uint32_t *sregs;
 #endif
 
 #ifdef CONFIG_ARCH_ADDRENV
diff --git a/arch/arm/src/armv7-a/arm_syscall.c 
b/arch/arm/src/armv7-a/arm_syscall.c
index 5c0039bf9b7..bcc9c187be5 100644
--- a/arch/arm/src/armv7-a/arm_syscall.c
+++ b/arch/arm/src/armv7-a/arm_syscall.c
@@ -513,6 +513,20 @@ uint32_t *arm_syscall(uint32_t *regs)
           rtcb->xcp.syscall[index].cpsr      = regs[REG_CPSR];
 #endif
 
+          /* Remember where the caller's registers are.  The cloning
+           * primitives need them:  the child of a fork() or vfork() made
+           * from user space resumes from this very SVC, so it is built from
+           * this frame and not from the registers the kernel-side stub
+           * happens to be running with.  Only the outermost system call is
+           * of interest, since that is the one the caller made.  See
+           * arm_fork().
+           */
+
+          if (index == 0)
+            {
+              rtcb->xcp.sregs = regs;
+            }
+
           regs[REG_PC]   = (uint32_t)dispatch_syscall;
 #ifdef CONFIG_BUILD_KERNEL
           cpsr           = regs[REG_CPSR] & ~PSR_MODE_MASK;
diff --git a/arch/arm/src/common/arm_fork.c b/arch/arm/src/common/arm_fork.c
index db822caee75..55adec0510f 100644
--- a/arch/arm/src/common/arm_fork.c
+++ b/arch/arm/src/common/arm_fork.c
@@ -42,75 +42,53 @@
 #include "sched/sched.h"
 
 /****************************************************************************
- * Public Functions
+ * Private Functions
  ****************************************************************************/
 
 /****************************************************************************
- * Name: arm_fork
+ * Name: arm_fork_direct
  *
  * Description:
- *   The common ARM worker behind up_fork().  vfork() and fork() snapshot
- *   the caller's registers identically; `vfork' says which primitive was
- *   called, and is passed straight through to nxtask_setup_fork(), which is
- *   where the memory semantics are decided.
- *
- *   What differs here is only the stack.  Normally the child has a stack of
- *   its own, and this function fills it with a relocated copy of the
- *   parent's, rebasing the stack and frame pointers to match.  When the
- *   child shares the parent's stack addresses -- a fork() child, inside its
- *   duplicated address environment -- there is nothing to relocate and the
- *   pointers are carried over unchanged.
- *
- *   The overall sequence is:
- *
- *   1) User code calls vfork() or fork().  The libc wrapper enters
- *      up_fork(), which collects context information and transfers control
- *      to arm_fork().
- *   2) arm_fork() calls nxtask_setup_fork().
- *   3) nxtask_setup_fork() allocates and configures the child task's TCB.
- *      This consists of:
- *      - Allocation of the child task's TCB.
- *      - Initialization of file descriptors and streams
- *      - Configuration of environment variables
- *      - Establishing the child's address environment:  joined to the
- *        parent's for vfork(), duplicated from it for fork()
- *      - Allocating the stack, or inheriting the parent's for fork()
- *      - Setup the input parameters for the task.
- *      - Initialization of the TCB (including call to up_initial_state())
- *   4) arm_fork() provides any additional operating context. arm_fork must:
- *      - Initialize special values in any CPU registers that were not
- *        already configured by up_initial_state()
- *   5) arm_fork() then calls nxtask_start_fork(), which for vfork()
- *      additionally suspends the caller.
- *   6) which executes the child thread.
- *
- * nxtask_abort_fork() may be called if an error occurs between steps 3 and
- * 6.
+ *   Clone a caller that reached up_fork() by an ordinary function call, so
+ *   that the register snapshot the entry point in fork.S took describes the
+ *   caller itself.  That is the case in a flat build, in a protected build
+ *   -- where a system call is dispatched on the caller's own stack, so the
+ *   caller's frames are copied along with the kernel-side ones and the child
+ *   unwinds back through them -- and for a kernel thread in any build.
  *
  * Input Parameters:
  *   vfork   - true for vfork(), false for fork()
+ *   parent  - The calling task's TCB
  *   context - Caller context information saved by the entry point
  *
  * Returned Value:
- *   Upon successful completion, 0 is returned to the child and the process
- *   ID of the child is returned to the parent.  Otherwise, -1 is returned to
- *   the parent, no child is created, and errno is set to indicate the error.
+ *   The process ID of the child, or ERROR on failure.
  *
  ****************************************************************************/
 
-pid_t arm_fork(bool vfork, const struct fork_s *context)
+static pid_t arm_fork_direct(bool vfork, struct tcb_s *parent,
+                             const struct fork_s *context)
 {
-  struct tcb_s *parent = this_task();
   struct tcb_s *child;
   uint32_t newsp;
   uint32_t newfp;
   uint32_t newtop;
   uint32_t stacktop;
   uint32_t stackutil;
-#ifdef CONFIG_ARCH_KERNEL_STACK
-  uint32_t oldsp = (uint32_t)parent->xcp.ustkptr;
-#else
   uint32_t oldsp = context->sp;
+
+#ifdef CONFIG_ARCH_KERNEL_STACK
+  /* A caller that trapped into a system call and was switched onto its
+   * kernel stack left its own stack pointer here; the snapshot in `context'
+   * is the kernel-side stub's.  Where the whole exception frame is available
+   * arm_fork_syscall() has already taken the call, so this is reached only
+   * where the caller's frames are copied along with the kernel-side ones.
+   */
+
+  if (parent->xcp.ustkptr != NULL)
+    {
+      oldsp = (uint32_t)parent->xcp.ustkptr;
+    }
 #endif
 
   sinfo("fork context [%p]:\n", context);
@@ -277,3 +255,219 @@ pid_t arm_fork(bool vfork, const struct fork_s *context)
 
   return nxtask_start_fork(child, vfork);
 }
+
+#if defined(CONFIG_ARCH_ARMV7A) && defined(CONFIG_ARCH_KERNEL_STACK)
+
+/****************************************************************************
+ * Name: arm_fork_syscall
+ *
+ * Description:
+ *   Clone a caller that reached up_fork() through a system call that was
+ *   switched onto a kernel stack.  The register snapshot fork.S took is
+ *   useless here:  it describes the kernel-side stub, and the frames below
+ *   it are on a stack the child does not get a copy of, so a child built
+ *   from it would resume at a kernel address with a stack pointer into its
+ *   own user stack.
+ *
+ *   What the caller was actually doing is the exception frame arm_vectorsvc
+ *   built on the caller's stack and arm_syscall() recorded in xcp.sregs; the
+ *   child is built from that.  It therefore returns from the very same SVC
+ *   instruction as the parent, in the same mode, differing only in that it
+ *   sees 0 as the return value and runs on its own stack.  The child is not
+ *   in a system call at all, so it inherits none of the parent's nesting
+ *   state.
+ *
+ * Input Parameters:
+ *   vfork  - true for vfork(), false for fork()
+ *   parent - The calling task's TCB
+ *
+ * Returned Value:
+ *   The process ID of the child, or ERROR on failure.
+ *
+ ****************************************************************************/
+
+static pid_t arm_fork_syscall(bool vfork, struct tcb_s *parent)
+{
+  uint32_t *sregs = parent->xcp.sregs;
+  struct tcb_s *child;
+  uint32_t newsp;
+  uint32_t newfp;
+  uint32_t newtop;
+  uint32_t stacktop;
+  uint32_t stackutil;
+  uint32_t oldsp = (uint32_t)parent->xcp.ustkptr;
+  uint32_t sysreturn;
+  uint32_t cpsr;
+
+  DEBUGASSERT(sregs != NULL && parent->xcp.nsyscalls > 0);
+
+  /* Where the caller resumes, and in which mode.  arm_syscall() re-pointed
+   * the frame at dispatch_syscall() before this was reached, so these two
+   * come from where it put the originals rather than from the frame.  In a
+   * protected build there is no mode change, so the frame still holds the
+   * caller's CPSR.
+   */
+
+  sysreturn = parent->xcp.syscall[0].sysreturn;
+#ifdef CONFIG_BUILD_KERNEL
+  cpsr      = parent->xcp.syscall[0].cpsr;
+#else
+  cpsr      = sregs[REG_CPSR];
+#endif
+
+  /* Allocate and initialize a TCB for the child task. */
+
+  child = nxtask_setup_fork((start_t)(sysreturn & ~1), vfork);
+  if (!child)
+    {
+      serr("ERROR: nxtask_setup_fork failed\n");
+      return (pid_t)ERROR;
+    }
+
+  stacktop = (uint32_t)parent->stack_base_ptr +
+                       parent->adj_stack_size;
+  DEBUGASSERT(stacktop > oldsp && oldsp >= (uint32_t)parent->stack_base_ptr);
+  stackutil = stacktop - oldsp;
+
+  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;
+       * see the same case in arm_fork_direct().
+       */
+
+      newsp = oldsp;
+      newfp = sregs[REG_FP];
+    }
+  else
+    {
+      newtop = (uint32_t)child->stack_base_ptr +
+                         child->adj_stack_size;
+      newsp  = newtop - stackutil;
+
+      /* Put the child's register save area where the parent's is:  just
+       * below the stack the caller was using.  It cannot be left at the top
+       * of the child's stack, which is where up_initial_state() put it,
+       * because the copy of the parent's stack below is about to land there.
+       */
+
+      child->xcp.regs = (uint32_t *)(newsp - XCPTCONTEXT_SIZE);
+
+      memcpy((void *)newsp, (const void *)oldsp, stackutil);
+
+      /* Was there a frame pointer in place before? */
+
+      if (sregs[REG_FP] >= oldsp && sregs[REG_FP] < stacktop)
+        {
+          uint32_t frameutil = stacktop - sregs[REG_FP];
+          newfp = newtop - frameutil;
+        }
+      else
+        {
+          newfp = sregs[REG_FP];
+        }
+    }
+
+  /* Inherit the caller's whole exception frame, integer and floating point
+   * registers alike, then fix up only what has to differ:  the child sees 0
+   * as the return value and runs on its own stack.
+   */
+
+  memcpy(child->xcp.regs, sregs, XCPTCONTEXT_SIZE);
+
+  child->xcp.regs[REG_R0]   = 0;
+  child->xcp.regs[REG_FP]   = newfp;
+  child->xcp.regs[REG_SP]   = newsp;
+  child->xcp.regs[REG_PC]   = sysreturn;
+  child->xcp.regs[REG_CPSR] = cpsr;
+
+  /* And, finally, start the child task.  A vfork() additionally suspends us
+   * until the child calls _exit() or exec().
+   */
+
+  return nxtask_start_fork(child, vfork);
+}
+
+#endif /* CONFIG_ARCH_ARMV7A && CONFIG_ARCH_KERNEL_STACK */
+
+/****************************************************************************
+ * Public Functions
+ ****************************************************************************/
+
+/****************************************************************************
+ * Name: arm_fork
+ *
+ * Description:
+ *   The common ARM worker behind up_fork().  vfork() and fork() snapshot
+ *   the caller's registers identically; `vfork' says which primitive was
+ *   called, and is passed straight through to nxtask_setup_fork(), which is
+ *   where the memory semantics are decided.
+ *
+ *   What differs here is only the stack.  Normally the child has a stack of
+ *   its own, and this function fills it with a relocated copy of the
+ *   parent's, rebasing the stack and frame pointers to match.  When the
+ *   child shares the parent's stack addresses -- a fork() child, inside its
+ *   duplicated address environment -- there is nothing to relocate and the
+ *   pointers are carried over unchanged.
+ *
+ *   The overall sequence is:
+ *
+ *   1) User code calls vfork() or fork().  The libc wrapper enters
+ *      up_fork(), which collects context information and transfers control
+ *      to arm_fork().
+ *   2) arm_fork() calls nxtask_setup_fork().
+ *   3) nxtask_setup_fork() allocates and configures the child task's TCB.
+ *      This consists of:
+ *      - Allocation of the child task's TCB.
+ *      - Initialization of file descriptors and streams
+ *      - Configuration of environment variables
+ *      - Establishing the child's address environment:  joined to the
+ *        parent's for vfork(), duplicated from it for fork()
+ *      - Allocating the stack, or inheriting the parent's for fork()
+ *      - Setup the input parameters for the task.
+ *      - Initialization of the TCB (including call to up_initial_state())
+ *   4) arm_fork() provides any additional operating context. arm_fork must:
+ *      - Initialize special values in any CPU registers that were not
+ *        already configured by up_initial_state()
+ *   5) arm_fork() then calls nxtask_start_fork(), which for vfork()
+ *      additionally suspends the caller.
+ *   6) which executes the child thread.
+ *
+ * nxtask_abort_fork() may be called if an error occurs between steps 3 and
+ * 6.
+ *
+ *   Everything above is common to the two ways this can be reached, which
+ *   differ only in where the caller's registers are to be found -- see
+ *   arm_fork_direct() and arm_fork_syscall().
+ *
+ * Input Parameters:
+ *   vfork   - true for vfork(), false for fork()
+ *   context - Caller context information saved by the entry point
+ *
+ * Returned Value:
+ *   Upon successful completion, 0 is returned to the child and the process
+ *   ID of the child is returned to the parent.  Otherwise, -1 is returned to
+ *   the parent, no child is created, and errno is set to indicate the error.
+ *
+ ****************************************************************************/
+
+pid_t arm_fork(bool vfork, const struct fork_s *context)
+{
+  struct tcb_s *parent = this_task();
+
+#if defined(CONFIG_ARCH_ARMV7A) && defined(CONFIG_ARCH_KERNEL_STACK)
+  /* A saved user stack pointer means this was reached through a system call
+   * that switched to the task's kernel stack, so the caller is the user task
+   * that trapped and not the code that called into fork.S.  A kernel thread
+   * has no kernel stack to switch to and never gets here with one saved.
+   */
+
+  if (parent->xcp.ustkptr != NULL)
+    {
+      DEBUGASSERT((parent->flags & TCB_FLAG_SYSCALL) != 0);
+      return arm_fork_syscall(vfork, parent);
+    }
+#endif
+
+  return arm_fork_direct(vfork, parent, context);
+}

Reply via email to