From 067e30e14f3e64a3b3d0bc6e63e23da2ce8ca39b Mon Sep 17 00:00:00 2001 From: Marco Casaroli Date: Sun, 9 Aug 2026 15:13:39 +0200 Subject: [PATCH] arch/x86_64: Build fork() children from the caller's syscall frame. In a kernel build vfork() is reached through a system call, so the return address and stack pointer the architecture's entry point can see for itself are the kernel's, not the caller's. A child built from those resumes at a kernel address, which is why x86_64 selected the fork family only for the flat build. x86_64_syscall() now publishes the caller's frame in xcp.sregs for the duration of the stub call, and x86_64_fork() builds the child from it: x86_64_fork_syscall() when xcp.sregs is non-NULL, so that the child returns from the very same `syscall' instruction as the parent, in user mode, on its own stack; x86_64_fork_direct() otherwise, which is the flat build and any kernel thread that calls the entry point as a plain function. The discriminator is xcp.sregs rather than TCB_FLAG_SYSCALL, which arm64 and RISC-V use: that flag also defers signal actions, x86_64 has never raised it, and its kernel-build signal path does not survive being made to -- a pre-existing problem that does not belong to this work. Two properties of SYSCALL/SYSRET shape the child's frame. The instruction leaves the caller's RIP and RFLAGS in RCX and R11 rather than on a stack, so they are moved into the RIP and RFLAGS slots of the interrupt frame the child is resumed from; and the hardware never records the caller's CS and SS at all, SYSRETQ reconstructing them from IA32_STAR, so the child's are filled in with the user code and data selectors at RPL 3. The frame is therefore not copied wholesale: the extended state and the general registers are inherited, while the segment registers and the thread pointer stay as up_initial_state() left them, the child's stack being a fresh allocation the parent's FS base does not describe. x86_64_fork_relocfp() is new and is not optional here. A function returns with `leave', which feeds the frame pointer into the stack pointer, so relocating only the RBP the child resumes with gets it exactly one frame: the next return loads a saved RBP still pointing into the parent's stack. With that in place ARCH_X86_64 can select ARCH_HAVE_VFORK unconditionally. Build-verified on qemu-intel64:knsh_romfs and qemu-intel64:ostest. NuttX on qemu-intel64 requires tsc-deadline and pcid, which TCG does not implement, so it cannot be run on this host. Assisted-by: Claude Code:claude-opus-5 Signed-off-by: Marco Casaroli --- Documentation/guides/fork_vfork_migration.rst | 17 +- arch/Kconfig | 2 +- arch/x86_64/include/intel64/irq.h | 12 + arch/x86_64/src/common/x86_64_fork.c | 447 +++++++++++++++--- arch/x86_64/src/common/x86_64_syscall.c | 35 +- 5 files changed, 450 insertions(+), 63 deletions(-) diff --git a/Documentation/guides/fork_vfork_migration.rst b/Documentation/guides/fork_vfork_migration.rst index 63e5f682a03..fa9a305d3c8 100644 --- a/Documentation/guides/fork_vfork_migration.rst +++ b/Documentation/guides/fork_vfork_migration.rst @@ -153,7 +153,7 @@ 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. -Three architectures do it, and they are worth copying: +Four 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. @@ -177,6 +177,21 @@ Three architectures do it, and they are worth copying: ``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``. +* x86_64: ``x86_64_syscall()`` stores the frame in ``xcp.sregs``, and + ``x86_64_fork()`` dispatches to ``x86_64_fork_syscall()`` or + ``x86_64_fork_direct()``. The discriminator here is ``xcp.sregs`` itself + being non-NULL, because raising ``TCB_FLAG_SYSCALL`` would also defer signal + actions -- something x86_64 has never done and its kernel-build signal path + does not currently survive. Two properties of ``SYSCALL``/``SYSRET`` shape + the child's frame: the instruction leaves the caller's RIP and RFLAGS in + RCX and R11 rather than on a stack, so they have to be moved into the RIP + and RFLAGS slots of the interrupt frame the child is resumed from; and the + hardware never records the caller's CS and SS at all -- ``SYSRETQ`` + reconstructs them from ``IA32_STAR`` -- so the child's have to be filled in + with the user code and data selectors at RPL 3. For the same reason the + saved frame is not copied wholesale: only the extended state and the + general registers are inherited, and the segment registers and thread + pointer come from the frame ``up_initial_state()`` built for the child. Nothing else is required: the ``up_fork()`` entry point and the libc wrapper are already there and become live automatically. diff --git a/arch/Kconfig b/arch/Kconfig index 3944e83d5a5..60039f1991e 100644 --- a/arch/Kconfig +++ b/arch/Kconfig @@ -147,7 +147,7 @@ config ARCH_X86_64 select PCI_LATE_DRIVERS_REGISTER if PCI select ARCH_TOOLCHAIN_GNU select ARCH_HAVE_BACKTRACE - select ARCH_HAVE_VFORK if !BUILD_KERNEL + select ARCH_HAVE_VFORK select ARCH_HAVE_SETJMP select ARCH_HAVE_PERF_EVENTS select ARCH_HAVE_POWEROFF diff --git a/arch/x86_64/include/intel64/irq.h b/arch/x86_64/include/intel64/irq.h index 24fd088d29c..84a04a4436f 100644 --- a/arch/x86_64/include/intel64/irq.h +++ b/arch/x86_64/include/intel64/irq.h @@ -544,6 +544,18 @@ struct xcptcontext uint64_t *regs; +#ifdef CONFIG_LIB_SYSCALL + /* The register context of the user code that is currently in a system + * call, as x86_64_syscall_entry() saved it on the kernel stack. This is + * what the caller of a system call was doing, as opposed to xcp.regs, + * which during a system call describes the kernel side of it. + * x86_64_fork() needs it to build a child from the caller rather than + * from the stub. + */ + + uint64_t *sregs; +#endif + #ifdef CONFIG_ARCH_ADDRENV # ifdef CONFIG_ARCH_KERNEL_STACK /* In this configuration, all syscalls execute from an internal kernel diff --git a/arch/x86_64/src/common/x86_64_fork.c b/arch/x86_64/src/common/x86_64_fork.c index 05cd26016e8..ef5cfb83e45 100644 --- a/arch/x86_64/src/common/x86_64_fork.c +++ b/arch/x86_64/src/common/x86_64_fork.c @@ -35,6 +35,7 @@ #include #include +#include #include #include "x86_64_fork.h" @@ -42,57 +43,156 @@ #include "sched/sched.h" /**************************************************************************** - * Public Functions + * Pre-processor Definitions + ****************************************************************************/ + +#ifdef CONFIG_LIB_SYSCALL + +/* Requested privilege level 3 in a segment selector. A caller that reached + * here through the `syscall' instruction was in user mode, and SYSRETQ is + * going to put it back there with the selectors IA32_STAR describes -- see + * x86_64_fork_syscall(). + */ + +# define X86_GDT_RPL_USER 3 + +#endif + +/**************************************************************************** + * Private Functions ****************************************************************************/ /**************************************************************************** - * Name: x86_64_fork + * Name: x86_64_fork_stacktop * * Description: - * The fork() function has the same effect as posix fork(), except that the - * behavior is undefined if the process created by fork() either modifies - * any data other than a variable of type pid_t used to store the return - * value from fork(), or returns from the function in which fork() was - * called, or calls any other function before successfully calling _exit() - * or one of the exec family of functions. - * - * The overall sequence is: - * - * 1) User code calls fork(). fork() collects context information and - * transfers control up x86_64_fork(). - * 2) x86_64_fork() and 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 - * - Allocate and initialize the stack - * - Setup the input parameters for the task. - * - Initialization of the TCB (including call to up_initial_state()) - * 4) x86_64_fork() provides any additional operating context. It must: - * - Initialize special values in any CPU registers that were not - * already configured by up_initial_state() - * 5) x86_64_fork() then calls nxtask_start_fork() - * 6) nxtask_start_fork() then executes the child thread. - * - * nxtask_abort_fork() may be called if an error occurs between steps 3 and - * 6. + * The high end of the part of a task's stack that a fork copies: the + * bottom of the register save area that up_initial_state() reserved at the + * very top of the stack. The save area itself is not stack and must not + * be copied over -- it is where the child's own resume frame is built. * * Input Parameters: - * vfork - true for vfork(), false for fork() - * context - Caller context information saved by up_fork() + * tcb - The task whose stack is in question * * Returned Value: - * Upon successful completion, fork() returns 0 to the child process and - * returns the process ID of the child process to the parent process. - * Otherwise, -1 is returned to the parent, no child process is created, - * and errno is set to indicate the error. + * The address one past the last byte of stack that is copied. * ****************************************************************************/ -pid_t x86_64_fork(bool vfork, const struct fork_s *context) +static uint64_t x86_64_fork_stacktop(struct tcb_s *tcb) +{ + return (uint64_t)XCP_ALIGN_DOWN((uintptr_t)tcb->stack_base_ptr + + tcb->adj_stack_size - XCPTCONTEXT_SIZE); +} + +/**************************************************************************** + * Name: x86_64_fork_reloc + * + * Description: + * Carry one address from the parent's stack over to the child's copy of + * it. Addresses outside the copied region are returned unchanged: they + * point somewhere the child shares with the parent, or somewhere that has + * no counterpart at all. + * + * Input Parameters: + * addr - The address to relocate + * rsp - The parent's stack pointer where the primitive was called, + * which is the low end of the region that was copied + * stacktop - The high end of the region that was copied + * offset - The distance from the parent's stack to the child's copy + * + * Returned Value: + * The relocated address. + * + ****************************************************************************/ + +static uint64_t x86_64_fork_reloc(uint64_t addr, uint64_t rsp, + uint64_t stacktop, uint64_t offset) +{ + if (addr >= rsp && addr < stacktop) + { + return addr + offset; + } + + return addr; +} + +/**************************************************************************** + * Name: x86_64_fork_relocfp + * + * Description: + * Relocate the saved frame-pointer chain inside the child's copy of the + * parent's stack. + * + * This is x86_64-specific and it is not optional. A function returns here + * with `leave', which is `mov %rbp,%rsp' followed by `pop %rbp': the + * frame pointer feeds the *stack* pointer. Relocating only the RBP the + * child resumes with therefore gets it exactly one frame; the moment it + * returns through the next one it loads a saved RBP that still points + * into the parent's stack, and from then on the child runs on the + * parent's stack. It looks like the child is working -- it is even at the + * right offset -- until something returns through a slot the parent has + * since reused. + * + * The other architectures with this fork path do not need it: they return + * through a link register, so a stale frame pointer spoils a backtrace and + * nothing else. + * + * The walk stops at the first link that leaves the copied region -- the + * outermost frame's saved RBP does -- and refuses to move backwards, so a + * corrupt chain terminates it rather than looping. + * + * Input Parameters: + * rbp - The parent's frame pointer where the primitive was called + * rsp - The parent's stack pointer, the low end of the copied region + * stacktop - The high end of the copied region + * offset - The distance from the parent's stack to the child's copy + * + ****************************************************************************/ + +static void x86_64_fork_relocfp(uint64_t rbp, uint64_t rsp, + uint64_t stacktop, uint64_t offset) +{ + while (rbp >= rsp && rbp < stacktop) + { + uint64_t *slot = (uint64_t *)(rbp + offset); + uint64_t next = *slot; + + if (next <= rbp || next >= stacktop) + { + break; + } + + *slot = next + offset; + rbp = next; + } +} + +/**************************************************************************** + * Name: x86_64_fork_direct + * + * Description: + * Clone a caller that reached up_fork() by an ordinary function call, so + * that the register snapshot fork.S took describes the caller itself. + * That is the case in a flat build, and for a kernel thread in any build. + * + * The child has no exception frame to inherit, so one is synthesised: it + * resumes at the caller's return address, in the caller's own segments, + * with the callee-saved registers the caller had. + * + * Input Parameters: + * vfork - true for vfork(), false for fork() + * parent - The calling task's TCB + * context - Caller context information saved by fork.S + * + * Returned Value: + * The process ID of the child, or ERROR on failure. + * + ****************************************************************************/ + +static pid_t x86_64_fork_direct(bool vfork, struct tcb_s *parent, + const struct fork_s *context) { - struct tcb_s *parent = this_task(); struct tcb_s *child; uint64_t newsp; uint64_t newfp; @@ -120,15 +220,13 @@ pid_t x86_64_fork(bool vfork, const struct fork_s *context) sinfo("TCBs: Parent=%p Child=%p\n", parent, child); - /* How much of the parent's stack was utilized? The ARM uses - * a push-down stack so that the current stack pointer should - * be lower than the initial, adjusted stack pointer. The - * stack usage should be the difference between those two. + /* How much of the parent's stack was utilized? x86_64 uses a push-down + * stack so that the current stack pointer should be lower than the + * initial, adjusted stack pointer. The stack usage should be the + * difference between those two. */ - stacktop = (uint64_t)XCP_ALIGN_DOWN((uintptr_t)parent->stack_base_ptr + - parent->adj_stack_size - - XCPTCONTEXT_SIZE); + stacktop = x86_64_fork_stacktop(parent); DEBUGASSERT(stacktop > context->rsp); stackutil = stacktop - context->rsp; @@ -161,25 +259,17 @@ pid_t x86_64_fork(bool vfork, const struct fork_s *context) * effort: the child is entitled to use them, and it does. */ - newtop = (uint64_t)XCP_ALIGN_DOWN((uintptr_t)child->stack_base_ptr + - child->adj_stack_size - - XCPTCONTEXT_SIZE); - - newsp = newtop - stackutil; + newtop = x86_64_fork_stacktop(child); + newsp = newtop - stackutil; memcpy((void *)newsp, (const void *)context->rsp, stackutil); /* Was there a frame pointer in place before? */ - if (context->rbp >= context->rsp && context->rbp < stacktop) - { - uint32_t frameutil = stacktop - context->rbp; - newfp = newtop - frameutil; - } - else - { - newfp = context->rbp; - } + newfp = x86_64_fork_reloc(context->rbp, context->rsp, stacktop, + newtop - stacktop); + x86_64_fork_relocfp(context->rbp, context->rsp, stacktop, + newtop - stacktop); sinfo("Old stack top:%08" PRIx64 " RSP:%08" PRIx64 " RBP:%08" PRIx64 "\n", stacktop, context->rsp, context->rbp); @@ -213,3 +303,242 @@ pid_t x86_64_fork(bool vfork, const struct fork_s *context) return nxtask_start_fork(child, vfork); } + +#ifdef CONFIG_LIB_SYSCALL + +/**************************************************************************** + * Name: x86_64_fork_syscall + * + * Description: + * Clone a caller that reached up_fork() through a system call. The + * register snapshot fork.S took is useless here: it describes the + * kernel-side stub, so a child built from it would resume at a kernel + * address on a kernel stack. What the caller was actually doing is the + * frame x86_64_syscall_entry() saved and x86_64_syscall() recorded in + * xcp.sregs; the child is built from that. + * + * The child therefore returns from the very same `syscall' instruction as + * the parent, in user mode, differing only in that it sees 0 as the return + * value and runs on its own stack. + * + * Two details of the SYSCALL/SYSRET pair shape this: + * + * 1. `syscall' does not save the caller's RIP and RFLAGS on a stack; it + * leaves them in RCX and R11, which is where the saved frame has them. + * The child is resumed by IRETQ (x86_64_fullcontextrestore()), so they + * have to be moved into the RIP and RFLAGS slots of its frame. + * 2. The hardware never tells the kernel which CS and SS the caller had -- + * SYSRETQ reconstructs them from IA32_STAR -- so those slots of the + * saved frame hold nothing, and the child's have to be filled with the + * selectors SYSRETQ would have produced, which is where the parent is + * about to return to. + * + * Everything the frame does hold -- the general registers and the extended + * (FPU/SSE) state -- is inherited. Everything it does not is taken from + * the frame up_initial_state() built for the child, so that the child + * keeps its own segment registers and, importantly, its own thread + * pointer: the child's stack is a fresh allocation at a different virtual + * address, so the parent's FS base does not describe it. + * + * 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 x86_64_fork_syscall(bool vfork, struct tcb_s *parent) +{ + uint64_t *sregs = parent->xcp.sregs; + struct tcb_s *child; + uint64_t newsp; + uint64_t newtop; + uint64_t offset; + uint64_t stacktop; + uint64_t stackutil; + uint64_t rsp; + uint64_t rip; + + DEBUGASSERT(sregs != NULL); + + /* Where the caller was and what it was doing */ + + rsp = sregs[REG_RSP]; + rip = sregs[REG_RCX]; + + sinfo("syscall frame [%p]: RSP:%08" PRIx64 " RIP:%08" PRIx64 "\n", + sregs, rsp, rip); + + /* Allocate and initialize a TCB for the child task. The child resumes at + * the instruction after the `syscall', which is where the parent resumes + * too. + */ + + child = nxtask_setup_fork((start_t)rip, vfork); + if (!child) + { + serr("ERROR: nxtask_setup_fork failed\n"); + return (pid_t)ERROR; + } + + sinfo("TCBs: Parent=%p Child=%p\n", parent, child); + + /* Give the child the part of the parent's stack that is in use, copied to + * the same place in its own stack. The copy is aligned with the top of + * each stack rather than the bottom, so a single offset carries any + * address in the copied region from one to the other. + */ + + stacktop = x86_64_fork_stacktop(parent); + DEBUGASSERT(stacktop > rsp); + stackutil = stacktop - rsp; + + newtop = x86_64_fork_stacktop(child); + newsp = newtop - stackutil; + offset = newtop - stacktop; + + memcpy((void *)newsp, (const void *)rsp, stackutil); + + sinfo("Old stack top:%08" PRIx64 " RSP:%08" PRIx64 "\n", stacktop, rsp); + sinfo("New stack top:%08" PRIx64 " RSP:%08" PRIx64 "\n", newtop, newsp); + + /* Inherit the parent's extended (FPU/SSE) state, which + * x86_64_syscall_entry saved at the front of the frame, exactly where + * the child's belongs. + */ + + memcpy(child->xcp.regs, sregs, XCPTCONTEXT_XMM_AREA_SIZE); + + /* Inherit the general registers. RCX and R11 are included deliberately: + * SYSRETQ leaves the return address in RCX and RFLAGS in R11, so the + * parent resumes with those values and the child must too. + */ + + child->xcp.regs[REG_RBX] = sregs[REG_RBX]; + child->xcp.regs[REG_R8] = sregs[REG_R8]; + child->xcp.regs[REG_R9] = sregs[REG_R9]; + child->xcp.regs[REG_R10] = sregs[REG_R10]; + child->xcp.regs[REG_R11] = sregs[REG_R11]; + child->xcp.regs[REG_R12] = sregs[REG_R12]; + child->xcp.regs[REG_R13] = sregs[REG_R13]; + child->xcp.regs[REG_R14] = sregs[REG_R14]; + child->xcp.regs[REG_R15] = sregs[REG_R15]; + child->xcp.regs[REG_RCX] = sregs[REG_RCX]; + child->xcp.regs[REG_RDX] = sregs[REG_RDX]; + child->xcp.regs[REG_RSI] = sregs[REG_RSI]; + child->xcp.regs[REG_RDI] = sregs[REG_RDI]; + + /* The frame pointer moves with the stack it points into */ + + child->xcp.regs[REG_RBP] = x86_64_fork_reloc(sregs[REG_RBP], rsp, + stacktop, offset); + x86_64_fork_relocfp(sregs[REG_RBP], rsp, stacktop, offset); + + /* Build the interrupt frame the child is resumed from. RIP and RFLAGS + * come out of RCX and R11, and the selectors are the ones SYSRETQ derives + * from IA32_STAR: CS is the user code segment and SS the user data + * segment, both at RPL 3. See x86_64_cpu_priv_set(), which programs + * IA32_STAR. + */ + + child->xcp.regs[REG_RAX] = 0; + child->xcp.regs[REG_RIP] = rip; + child->xcp.regs[REG_RFLAGS] = sregs[REG_R11]; + child->xcp.regs[REG_RSP] = newsp; + child->xcp.regs[REG_CS] = X86_GDT_USERCODE_SEL | X86_GDT_RPL_USER; + child->xcp.regs[REG_SS] = X86_GDT_USERDATA_SEL | X86_GDT_RPL_USER; + +#ifdef CONFIG_ARCH_KERNEL_STACK + /* The child's own user stack pointer, for the signal dispatch path */ + + child->xcp.ustkptr = (uintptr_t *)newsp; +#endif + + /* And, finally, start the child task. On a failure, nxtask_start_fork() + * will discard the TCB by calling nxtask_abort_fork(). + */ + + return nxtask_start_fork(child, vfork); +} + +#endif /* CONFIG_LIB_SYSCALL */ + +/**************************************************************************** + * Public Functions + ****************************************************************************/ + +/**************************************************************************** + * Name: x86_64_fork + * + * Description: + * The common x86_64 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. + * + * 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 x86_64_fork(). + * 2) x86_64_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 + * - Allocate and initialize the stack + * - Setup the input parameters for the task. + * - Initialization of the TCB (including call to up_initial_state()) + * 4) x86_64_fork() provides any additional operating context. It must: + * - Initialize special values in any CPU registers that were not + * already configured by up_initial_state() + * 5) x86_64_fork() then calls nxtask_start_fork() + * 6) nxtask_start_fork() then 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 + * x86_64_fork_direct() and x86_64_fork_syscall(). + * + * Input Parameters: + * vfork - true for vfork(), false for fork() + * context - Caller context information saved by fork.S + * + * 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 x86_64_fork(bool vfork, const struct fork_s *context) +{ + struct tcb_s *parent = this_task(); + +#ifdef CONFIG_LIB_SYSCALL + /* A non-NULL xcp.sregs means a system call is in progress: + * x86_64_syscall() publishes the caller's frame there for the duration of + * the call and nowhere else. So this was reached from a kernel-side stub, + * and the caller to clone is the user task that trapped, not the code that + * called into fork.S. + * + * arm64 and RISC-V discriminate on TCB_FLAG_SYSCALL instead. x86_64 + * cannot: that flag also defers signal actions, which x86_64 has never + * done and which its kernel-build signal path does not currently survive + * -- see the note in x86_64_syscall(). xcp.sregs says exactly what is + * needed here and means nothing to anyone else. + */ + + if (parent->xcp.sregs != NULL) + { + return x86_64_fork_syscall(vfork, parent); + } +#endif + + return x86_64_fork_direct(vfork, parent, context); +} diff --git a/arch/x86_64/src/common/x86_64_syscall.c b/arch/x86_64/src/common/x86_64_syscall.c index 848f625c3ce..196453f5d6a 100644 --- a/arch/x86_64/src/common/x86_64_syscall.c +++ b/arch/x86_64/src/common/x86_64_syscall.c @@ -313,15 +313,42 @@ uint64_t *x86_64_syscall(uint64_t *regs) #ifdef CONFIG_LIB_SYSCALL int nbr = cmd - CONFIG_SYS_RESERVED; syscall_stub_t stub = (syscall_stub_t)g_stublookup[nbr]; + struct tcb_s *rtcb = nxsched_self(); + uint64_t *sregs; #ifdef CONFIG_ARCH_KERNEL_STACK - struct tcb_s *rtcb = nxsched_self(); - /* Store reference to user RSP for signals */ rtcb->xcp.saved_ursp = regs[REG_RSP]; #endif + /* Publish the caller's register context. up_fork() has to clone + * the caller rather than the stub that is about to invoke it, and + * this frame is the only description of it -- see + * x86_64_fork_syscall(), which also takes a non-NULL xcp.sregs as + * its "reached here through a system call" discriminator. + * + * It is saved and restored rather than simply set and cleared: + * x86_64_syscall_entry() has an explicit path for a nested system + * call, and when the inner one returns the outer one must still be + * described by its own frame. + * + * Note what is deliberately *not* done here. arm64 and RISC-V + * also raise TCB_FLAG_SYSCALL across the stub call, which defers + * any signal action until the system call returns. x86_64 has + * never set it, and making it do so is not free: the deferred + * action then has to be picked up by nxsig_unmask_pendingsignal() + * on the way out, and the signal dispatch path of an x86_64 kernel + * build does not survive that today -- it faults in + * x86_64_syscall_entry()'s return path with RSP == 0. That is a + * pre-existing bug in a configuration nothing has exercised, and + * fixing it does not belong to the fork/vfork work; so this + * records the frame and changes nothing else. + */ + + sregs = rtcb->xcp.sregs; + rtcb->xcp.sregs = regs; + /* Re-enable interrupts if enabled before. * Current task RFLAGS are stored in R11. */ @@ -334,6 +361,10 @@ uint64_t *x86_64_syscall(uint64_t *regs) /* Call syscall function and store return value in RAX register */ regs[REG_RAX] = stub(nbr, arg1, arg2, arg3, arg4, arg5, arg6); + + /* The system call is now done */ + + rtcb->xcp.sregs = sregs; #else svcerr("ERROR: Bad SYS call: %" PRId32 "\n", cmd); #endif