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 <marco.casaroli@gmail.com>
In a kernel or protected build vfork() is reached through a system call, so
the return address and stack pointer that the entry point in
arm64_fork_func.S can snapshot for itself belong to the kernel-side stub, not
to the caller. A child built from that snapshot resumes at a kernel address
on a kernel stack. This is why arm64 selected the fork family only for the
flat build.
Record what the caller was actually doing instead. arm64_sync_exc passes the
exception frame to dispatch_syscall() in x7 -- x0-x6 carry the call number and
its six parameters, so x7 is free -- and dispatch_syscall() stores it in
xcp.sregs, mirroring what riscv_swint.c does.
arm64_fork() then chooses where the caller's registers live:
arm64_fork_syscall() when TCB_FLAG_SYSCALL is set, rebuilding the child
from xcp.sregs so that it returns from the very same
SVC as the parent;
arm64_fork_direct() otherwise, which is the flat build and any kernel
thread that calls the entry point as a plain function.
The stack copy and the relocation of pointers into it are shared by both
paths in arm64_fork_stack() and arm64_fork_reloc().
With that in place ARCH_ARM64 can select ARCH_HAVE_VFORK unconditionally.
Verified on qemu-armv8a:knsh (BUILD_KERNEL), qemu-armv8a:nsh (BUILD_FLAT) and
qemu-armv8a:citest_smp under qemu-system-aarch64: ostest's vfork_test passes
on all three, and it was absent from knsh before the change. The protected
configurations are build-verified only (fvp-armv8r:pnsh), there being no
emulator for them here.
Assisted-by: Claude Code:claude-opus-5
Signed-off-by: Marco Casaroli <marco.casaroli@gmail.com>
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 <marco.casaroli@gmail.com>
Documentation/guides/fork_vfork_migration.rst is new. It says what changed
and why, gives the two primitives as a table, states plainly what breaks, and
answers "which replacement do I want?" from the reader's own reason for having
called fork() -- posix_spawn() or vfork() to run a program, pthread_create()
for a second flow of control that shares memory, fork() itself for an
independent copy. It also documents the two configuration symbols, what an
architecture has to implement to gain real fork(), and the one visible
consequence of moving the vfork() suspension into the kernel: a waitpid()
after a child that _exit()s can only report status where
CONFIG_SCHED_CHILD_STATUS is enabled.
reference/user/01_task_control.rst gains an entry for fork() and rewrites the
one for vfork(), which described NuttX's limitations rather than the
interface's contract. standards/posix.rst moves fork() from "No" to "Cond."
and vfork() from "Yes" to "Cond.", both being conditional on the configuration
now. implementation/memory_configurations.rst no longer lists fork() as
unimplementable in the presence of address environments, which was the whole
point of that section's wish list. Three long-standing typos in that file are
corrected while touching it, since codespell checks the whole of any file a
patch modifies.
BREAKING CHANGE: this commit carries no code; it is the migration guide for
the fork() withdrawal in the commit before it, and is marked so that every
commit in the series carries the marker CONTRIBUTING.md 1.13 requires. The
quick fixes are in Documentation/guides/fork_vfork_migration.rst.
Assisted-by: Claude Code:claude-opus-5
Signed-off-by: Marco Casaroli <marco.casaroli@gmail.com>