!sched/arch/libc: Give fork() and vfork() their real, separate semantics. - #19562
!sched/arch/libc: Give fork() and vfork() their real, separate semantics.#19562casaroli wants to merge 2 commits into
Conversation
|
|
I was planning on having one follow up PR for each relevant architecture, however I now think maybe we should keep one of the architectures that supports these are MMU capable, in order of complexity (low to high):
|
f8d329e to
e5675dc
Compare
jerpelea
left a comment
There was a problem hiding this comment.
please replace
Co-Authored-By: Claude Opus 5 (1M context) noreply@anthropic.com
with
Assisted-by: Claude Opus 5 (1M context) noreply@anthropic.com
0132031 to
9b787d4
Compare
🔗 Cross-repo PR dependenciesThe read-only Build run reported the following dependent PR(s) and fetched head SHA(s):
CI run: https://github.com/apache/nuttx/actions/runs/30747464194 |
🔗 Cross-repo PR dependenciesThe read-only Build run reported the following dependent PR(s) and fetched head SHA(s):
CI run: https://github.com/apache/nuttx/actions/runs/30747546033 |
Three places call fork() from code that is compiled unconditionally, which is fine only for as long as every architecture provides it. NuttX is splitting fork() into three primitives -- see apache/nuttx#19562 -- after which ARCH_HAVE_FORK announces POSIX fork() specifically, and is off until an architecture implements it. These three then fail to link. system/libuv: test-fork.c and test-pipe-close-stdout-read-stdin.c are filtered out of the test-*.c glob. Every test they define is already excluded from the task list on NuttX by 0001-libuv-port-for-nuttx.patch, so they were dead code that compiled only because fork() happened to be declared. Nothing is lost. testing/ltp: the open_posix_testsuite is filtered through LTP's existing BLACKWORDS mechanism, which already drops tests for absent features and is already conditioned on configuration symbols. The pattern spares vfork() and task_fork(). Where fork() is absent this drops 278 of 1943 test files; those tests exercise fork() and cannot link without it, and they return per architecture as fork() lands. testing/drivers/nand_sim: gains in Kconfig the dependency it always had implicitly. It accepts either primitive -- it wants a daemon that outlives its caller and shares its memory, which today's fork() provides and task_fork() will provide after the split -- so gating it on ARCH_HAVE_FORK alone would silently drop it from the two sim configurations that enable it once that symbol is withdrawn. Against today's master this is a no-op: ARCH_HAVE_FORK is set everywhere, so nothing is filtered and nothing is disabled. It is what lets the NuttX side build against apps master. Assisted-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Marco Casaroli <marco.casaroli@gmail.com>
NuttX implements fork() and vfork() as the same function and is gaining three separate primitives -- see apache/nuttx#19562: task_fork() (shares memory, private stack copy, both running), vfork() (shares memory, parent suspended until _exit()/exec()) and POSIX fork() (child gets its own copy). This gives each one a test of its own. ostest's "vfork" test was never testing vfork(). It has the child write a global and the parent observe the write -- the defining property of *sharing*, not of vfork(), whose defining property is that the parent is suspended and whose contract forbids the child to write anything at all. It is renamed to task_fork.c, unchanged, because that is the primitive it has always described. vfork.c is rewritten to test what vfork() promises. The child does only what POSIX permits: it calls _exit(42) and nothing else, not even exit(), which would run atexit handlers and flush stdio in the parent's address space. Since the child may not write memory and the parent cannot run while the child lives, the observable is the child's exit status -- had the parent not been suspended it would have reached waitpid() while the child was still alive. Where child status is not retained, because ostest_main() sets SA_NOCLDWAIT for the whole run, ECHILD is accepted as equally good evidence. fork.c is new and tests POSIX fork(): the child's writes to .data, .bss and the heap are invisible to the parent and vice versa, a pointer to a stack local taken before the fork names the same object in both, and the child does everything a vfork() child may not -- calls malloc() and printf(), and returns from the function that called fork(). All three run at the top of user_main(). They exercise the lowest-level machinery in the suite -- address environments, stack setup, the architecture's register context -- so a fault in one takes the process down instead of reporting a failure. Learning that in seconds rather than after everything else has passed matters when a port is being brought up. Each test gates on the one primitive it tests and nothing stands in for anything. task_fork_test() keys on CONFIG_TASK_FORK rather than the capability symbol: ARCH_HAVE_TASK_FORK says the architecture can clone a task, TASK_FORK says this build asked for it, and task_fork() is declared only under the latter. TESTING_NAND_SIM depends on the same symbol for the same reason -- it wants a daemon that outlives its caller and shares its memory, which is task_fork(). vfork_test() and fork_test() have no such split and key on ARCH_HAVE_VFORK and ARCH_HAVE_FORK directly. The other in-tree callers are audited for which primitive they meant: python's _posixsubprocess and libwebsockets' LWS_HAVE_WORKING_VFORK want the fork-then-exec path, so they follow ARCH_HAVE_VFORK; python's os.fork() and libwebsockets' LWS_HAVE_FORK mean real fork() and stay on ARCH_HAVE_FORK, so they become absent rather than silently wrong; fdsantest's vfork case follows vfork(). Depends on apache/nuttx#19562 and must not merge before it. Assisted-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Marco Casaroli <marco.casaroli@gmail.com>
9b787d4 to
82f8e8b
Compare
🔗 Cross-repo PR dependenciesThe read-only Build run reported the following dependent PR(s) and fetched head SHA(s):
CI run: https://github.com/apache/nuttx/actions/runs/30753741821 |
Two places call fork() from code that is compiled unconditionally, which is fine only for as long as every architecture provides it. NuttX is splitting fork() into three primitives -- see apache/nuttx#19562 -- after which ARCH_HAVE_FORK announces POSIX fork() specifically, and is off until an architecture implements it. Both then fail to link. system/libuv: test-fork.c and test-pipe-close-stdout-read-stdin.c are filtered out of the test-*.c glob. Every test they define is already excluded from the task list on NuttX by 0001-libuv-port-for-nuttx.patch, so they were dead code that compiled only because fork() happened to be declared. Nothing is lost. testing/ltp: the open_posix_testsuite is filtered through LTP's existing BLACKWORDS mechanism, which already drops tests for absent features and is already conditioned on configuration symbols. The pattern spares vfork() and task_fork(). Where fork() is absent this drops 278 of 1943 test files; those tests exercise fork() and cannot link without it, and they return per architecture as fork() lands. Against today's master this is a no-op: ARCH_HAVE_FORK is set everywhere, so nothing is filtered. It is part of what lets the NuttX side build against apps master. Assisted-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Marco Casaroli <marco.casaroli@gmail.com>
…ground. Neither of these wants fork() semantics. Both reach for fork() only to put work in the background, and each has a NuttX-native way to do that, so neither needs a fork primitive at all -- which matters once apache/nuttx#19562 makes ARCH_HAVE_FORK conditional on the architecture implementing POSIX fork(). netutils/dropbear: the port already routes every fork-then-exec through vfork(), because sysoptions.h selects DROPBEAR_VFORK when HAVE_FORK is undefined and the port leaves it undefined. spawn_command() in dbutil.c and both call sites in scp.c follow that switch. The one exception is the daemon() fallback that compat.c compiles under #ifndef HAVE_DAEMON, which calls fork() directly and bypasses it. NuttX provides daemon() in libs/libc/unistd/lib_daemon.c and declares it in unistd.h, so the fallback is redundant; define HAVE_DAEMON alongside the HAVE_STRLCAT and HAVE_STRLCPY entries that are there for exactly the same reason. The code was unreachable in any case -- the port hands svr_getopts() an argv containing -F, so svr_opts.forkbg is always zero and dropbear never calls daemon() at all. testing/drivers/nand_sim: forked so that the parent could return to the shell while the child registered the MTD device and slept forever. Nothing from before the fork is used after it, so the child is a self-contained entry point, and task_create() expresses that directly. The emulator body moves into nand_sim_daemon() unchanged. TESTING_NAND_SIM therefore needs no fork dependency, and the two sim configurations that enable it keep working whatever ARCH_HAVE_FORK is set to. Assisted-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Marco Casaroli <marco.casaroli@gmail.com>
NuttX implements fork() and vfork() as the same function and is gaining three separate primitives -- see apache/nuttx#19562: task_fork() (shares memory, private stack copy, both running), vfork() (shares memory, parent suspended until _exit()/exec()) and POSIX fork() (child gets its own copy). This gives each one a test of its own. ostest's "vfork" test was never testing vfork(). It has the child write a global and the parent observe the write -- the defining property of *sharing*, not of vfork(), whose defining property is that the parent is suspended and whose contract forbids the child to write anything at all. It is renamed to task_fork.c, unchanged, because that is the primitive it has always described. vfork.c is rewritten to test what vfork() promises. The child does only what POSIX permits: it calls _exit(42) and nothing else, not even exit(), which would run atexit handlers and flush stdio in the parent's address space. Since the child may not write memory and the parent cannot run while the child lives, the observable is the child's exit status -- had the parent not been suspended it would have reached waitpid() while the child was still alive. Where child status is not retained, because ostest_main() sets SA_NOCLDWAIT for the whole run, ECHILD is accepted as equally good evidence. fork.c is new and tests POSIX fork(): the child's writes to .data, .bss and the heap are invisible to the parent and vice versa, a pointer to a stack local taken before the fork names the same object in both, and the child does everything a vfork() child may not -- calls malloc() and printf(), and returns from the function that called fork(). All three run at the top of user_main(). They exercise the lowest-level machinery in the suite -- address environments, stack setup, the architecture's register context -- so a fault in one takes the process down instead of reporting a failure. Learning that in seconds rather than after everything else has passed matters when a port is being brought up. Each test gates on the one primitive it tests and nothing stands in for anything. task_fork_test() keys on CONFIG_TASK_FORK rather than the capability symbol: ARCH_HAVE_TASK_FORK says the architecture can clone a task, TASK_FORK says this build asked for it, and task_fork() is declared only under the latter. vfork_test() and fork_test() have no such split and key on ARCH_HAVE_VFORK and ARCH_HAVE_FORK directly. The other in-tree callers are audited for which primitive they meant: python's _posixsubprocess and libwebsockets' LWS_HAVE_WORKING_VFORK want the fork-then-exec path, so they follow ARCH_HAVE_VFORK; python's os.fork() and libwebsockets' LWS_HAVE_FORK mean real fork() and stay on ARCH_HAVE_FORK, so they become absent rather than silently wrong; fdsantest's vfork case follows vfork(). Depends on apache/nuttx#19562 and must not merge before it. Assisted-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Marco Casaroli <marco.casaroli@gmail.com>
Two places call fork() from code that is compiled unconditionally, which is fine only for as long as every architecture provides it. NuttX is splitting fork() into three primitives -- see apache/nuttx#19562 -- after which ARCH_HAVE_FORK announces POSIX fork() specifically, and is off until an architecture implements it. Both then fail to link. Each is dropped only where ARCH_HAVE_FORK is unset, so builds that have fork() are unaffected. system/libuv: test-fork.c and test-pipe-close-stdout-read-stdin.c are filtered out of the test-*.c glob. Nothing is lost even where they are dropped: every test they define is already excluded from the task list on NuttX by 0001-libuv-port-for-nuttx.patch, which extends the _WIN32 guards around them to __NuttX__ -- all nine fork_* entries and pipe_close_stdout_read_stdin. They are compiled today but never run. testing/ltp: the open_posix_testsuite is filtered through LTP's existing BLACKWORDS mechanism, which already drops tests for absent features and is already conditioned on configuration symbols. The pattern spares vfork() and task_fork(). Where fork() is absent this drops 278 of 1943 test files; those tests exercise fork() and cannot link without it, and they return per architecture as fork() lands. Against today's master this is a no-op: ARCH_HAVE_FORK is set everywhere, so neither filter drops anything. It is part of what lets the NuttX side build against apps master. Assisted-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Marco Casaroli <marco.casaroli@gmail.com>
…ground. Neither of these wants fork() semantics. Both reach for fork() only to put work in the background, and each has a NuttX-native way to do that, so neither needs a fork primitive at all -- which matters once apache/nuttx#19562 makes ARCH_HAVE_FORK conditional on the architecture implementing POSIX fork(). netutils/dropbear: the port already routes every fork-then-exec through vfork(), because sysoptions.h selects DROPBEAR_VFORK when HAVE_FORK is undefined and the port leaves it undefined. spawn_command() in dbutil.c and both call sites in scp.c follow that switch. The one exception is the daemon() fallback that compat.c compiles under #ifndef HAVE_DAEMON, which calls fork() directly and bypasses it. NuttX provides daemon() in libs/libc/unistd/lib_daemon.c and declares it in unistd.h, so the fallback is redundant; define HAVE_DAEMON alongside the HAVE_STRLCAT and HAVE_STRLCPY entries that are there for exactly the same reason. The code was unreachable in any case -- the port hands svr_getopts() an argv containing -F, so svr_opts.forkbg is always zero and dropbear never calls daemon() at all. testing/drivers/nand_sim: forked so that the parent could return to the shell while the child registered the MTD device and slept forever. Nothing from before the fork is used after it, so the child is a self-contained entry point, and task_create() expresses that directly. The emulator body moves into nand_sim_daemon() unchanged. TESTING_NAND_SIM therefore needs no fork dependency, and the two sim configurations that enable it keep working whatever ARCH_HAVE_FORK is set to. Assisted-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Marco Casaroli <marco.casaroli@gmail.com>
NuttX implements fork() and vfork() as the same function and is gaining three separate primitives -- see apache/nuttx#19562: task_fork() (shares memory, private stack copy, both running), vfork() (shares memory, parent suspended until _exit()/exec()) and POSIX fork() (child gets its own copy). This gives each one a test of its own. ostest's "vfork" test was never testing vfork(). It has the child write a global and the parent observe the write -- the defining property of *sharing*, not of vfork(), whose defining property is that the parent is suspended and whose contract forbids the child to write anything at all. It is renamed to task_fork.c, unchanged, because that is the primitive it has always described. vfork.c is rewritten to test what vfork() promises. The child does only what POSIX permits: it calls _exit(42) and nothing else, not even exit(), which would run atexit handlers and flush stdio in the parent's address space. Since the child may not write memory and the parent cannot run while the child lives, the observable is the child's exit status -- had the parent not been suspended it would have reached waitpid() while the child was still alive. Where child status is not retained, because ostest_main() sets SA_NOCLDWAIT for the whole run, ECHILD is accepted as equally good evidence. fork.c is new and tests POSIX fork(): the child's writes to .data, .bss and the heap are invisible to the parent and vice versa, a pointer to a stack local taken before the fork names the same object in both, and the child does everything a vfork() child may not -- calls malloc() and printf(), and returns from the function that called fork(). All three run at the top of user_main(). They exercise the lowest-level machinery in the suite -- address environments, stack setup, the architecture's register context -- so a fault in one takes the process down instead of reporting a failure. Learning that in seconds rather than after everything else has passed matters when a port is being brought up. Each test gates on the one primitive it tests and nothing stands in for anything. task_fork_test() keys on CONFIG_TASK_FORK rather than the capability symbol: ARCH_HAVE_TASK_FORK says the architecture can clone a task, TASK_FORK says this build asked for it, and task_fork() is declared only under the latter. vfork_test() and fork_test() have no such split and key on ARCH_HAVE_VFORK and ARCH_HAVE_FORK directly. The other in-tree callers are audited for which primitive they meant: python's _posixsubprocess and libwebsockets' LWS_HAVE_WORKING_VFORK want the fork-then-exec path, so they follow ARCH_HAVE_VFORK; python's os.fork() and libwebsockets' LWS_HAVE_FORK mean real fork() and stay on ARCH_HAVE_FORK, so they become absent rather than silently wrong; fdsantest's vfork case follows vfork(). Depends on apache/nuttx#19562 and must not merge before it. Assisted-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Marco Casaroli <marco.casaroli@gmail.com>
Two places call fork() from code that is compiled unconditionally, which is fine only for as long as every architecture provides it. NuttX is splitting fork() into three primitives -- see apache/nuttx#19562 -- after which ARCH_HAVE_FORK announces POSIX fork() specifically, and is off until an architecture implements it. Both then fail to link. Each is dropped only where ARCH_HAVE_FORK is unset, so builds that have fork() are unaffected. system/libuv: test-fork.c and test-pipe-close-stdout-read-stdin.c are filtered out of the test-*.c glob. Nothing is lost even where they are dropped: every test they define is already excluded from the task list on NuttX by 0001-libuv-port-for-nuttx.patch, which extends the _WIN32 guards around them to __NuttX__ -- all nine fork_* entries and pipe_close_stdout_read_stdin. They are compiled today but never run. testing/ltp: the open_posix_testsuite is filtered through LTP's existing BLACKWORDS mechanism, which already drops tests for absent features and is already conditioned on configuration symbols. The pattern spares vfork() and task_fork(). Where fork() is absent this drops 278 of 1943 test files; those tests exercise fork() and cannot link without it, and they return per architecture as fork() lands. Against today's master this is a no-op: ARCH_HAVE_FORK is set everywhere, so neither filter drops anything. It is part of what lets the NuttX side build against apps master. Assisted-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Marco Casaroli <marco.casaroli@gmail.com>
…ground. Neither of these wants fork() semantics. Both reach for fork() only to put work in the background, and each has a NuttX-native way to do that, so neither needs a fork primitive at all -- which matters once apache/nuttx#19562 makes ARCH_HAVE_FORK conditional on the architecture implementing POSIX fork(). netutils/dropbear: the port already routes every fork-then-exec through vfork(), because sysoptions.h selects DROPBEAR_VFORK when HAVE_FORK is undefined and the port leaves it undefined. spawn_command() in dbutil.c and both call sites in scp.c follow that switch. The one exception is the daemon() fallback that compat.c compiles under #ifndef HAVE_DAEMON, which calls fork() directly and bypasses it. NuttX provides daemon() in libs/libc/unistd/lib_daemon.c and declares it in unistd.h, so the fallback is redundant; define HAVE_DAEMON alongside the HAVE_STRLCAT and HAVE_STRLCPY entries that are there for exactly the same reason. The code was unreachable in any case -- the port hands svr_getopts() an argv containing -F, so svr_opts.forkbg is always zero and dropbear never calls daemon() at all. testing/drivers/nand_sim: forked so that the parent could return to the shell while the child registered the MTD device and slept forever. Nothing from before the fork is used after it, so the child is a self-contained entry point, and task_create() expresses that directly. The emulator body moves into nand_sim_daemon() unchanged. TESTING_NAND_SIM therefore needs no fork dependency, and the two sim configurations that enable it keep working whatever ARCH_HAVE_FORK is set to. Assisted-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Marco Casaroli <marco.casaroli@gmail.com>
82f8e8b to
9132156
Compare
ostest's "vfork" test was never testing vfork(). It has the child write a global and the parent observe the write -- the defining property of *sharing*, not of vfork(), whose defining property is that the parent is suspended and whose contract forbids the child to write anything at all. It passed because NuttX implemented fork() and vfork() as the same sharing primitive, which apache/nuttx#19562 separates. vfork.c is rewritten to test what vfork() promises. The child does only what POSIX permits -- it calls _exit(42) and nothing else, not even exit(), which would run atexit handlers and flush stdio in the parent's address space. Since the child may not write memory and the parent cannot run while the child lives, the observable is the child's exit status: had the parent not been suspended, it would have reached waitpid() while the child was still alive. Where child status is not retained -- ostest_main() sets SA_NOCLDWAIT for the whole run, deliberately -- ECHILD is accepted as equally good evidence, since it says the child was already gone when the parent asked. fork.c is new and tests POSIX fork(): the child's writes to .data, .bss and the heap are invisible to the parent and vice versa, a pointer to a stack local taken before the fork names the same object in both, and the child does everything a vfork() child may not -- calls malloc() and printf(), and returns from the function that called fork(). Both run at the top of user_main(). They exercise the lowest-level machinery in the suite -- address environments, stack setup, the architecture's register context -- so a fault in one takes the process down instead of reporting a failure. Learning that in seconds rather than after everything else has passed matters when a port is being brought up. Each test gates on the one primitive it tests, ARCH_HAVE_VFORK and ARCH_HAVE_FORK respectively. There is no compatibility layer and no mapping between symbols. vfork.c no longer requires SCHED_WAITPID: the suspension is in the kernel primitive now, so the test's core assertion holds without it and only the status check is conditional. The other in-tree callers are audited for which primitive they actually meant: * interpreters/python's _posixsubprocess and netutils/libwebsockets' LWS_HAVE_WORKING_VFORK want the fork-then-exec path -- ARCH_HAVE_VFORK. * python's os.fork() and libwebsockets' LWS_HAVE_FORK mean real fork() and stay on ARCH_HAVE_FORK, so they become *absent* rather than silently wrong. * testing/fs/fdsantest's vfork case follows ARCH_HAVE_VFORK. interpreters/bas is deliberately left alone. Its SHELL and EDIT statements reach for vfork() under an ARCH_HAVE_FORK guard and want the same treatment, but checkpatch.sh checks the whole of any file a patch touches and bas_statement.c produces 1681 pre-existing findings against master, so a one-line change there fails CI on its own. The consequence is small: EXAMPLES_BAS_SHELL is EXPERIMENTAL and already depends on ARCH_HAVE_FORK, so it becomes unselectable rather than misbehaving. Assisted-by: Claude Code:claude-opus-5 Signed-off-by: Marco Casaroli <marco.casaroli@gmail.com>
❌ Cross-repo dependency could not be appliedThe Build report says the declared dependency PR(s) could not be applied, so CI did not run against the combined code: Reason: cherry-pick failed (if your PR has merge commits, rebase instead) CI run: https://github.com/apache/nuttx/actions/runs/31246987967 |
9132156 to
df3ccde
Compare
…ics. NuttX implemented fork() and vfork() as the same function. Both were libc wrappers around a single up_fork() syscall; vfork() differed only by a trailing waitpid(). Underneath, the child joined the parent's address environment -- the same addrenv_join() that pthread_create() uses -- and got a private copy of the stack. So the child shared .data, .bss and the heap with its parent and ran concurrently with it. That is not fork(). It is vfork()-with-a-private-stack under fork()'s name, and the history says so: today's fork() is NuttX's old vfork(), renamed in c33d1c9 (2023) without any change of behaviour. The failure was silent -- a program written against POSIX fork() compiled, ran, and had its child's writes land in the parent's variables. Separate them into two primitives, chosen by which function the caller called rather than by what the hardware happens to be: fork() child gets its own copy of the parent's memory at the same virtual addresses; runs concurrently. Only where an address environment can be duplicated -- elsewhere it is not declared at all, so calling it is a build error naming the function. vfork() child shares the parent's memory; parent suspended until the child _exit()s or exec()s. Implementable everywhere. Below libc there is still one syscall. up_fork() gains a bool saying which primitive the caller used, since the per-architecture register snapshot is the same for both, and passes it to nxtask_setup_fork(), which is the single place the memory semantics are decided. The argument arrives in the first argument register and is never touched: each architecture's snapshot takes some other call-clobbered register for its scratch, so the flag is simply still there when the C worker is called. The vfork() parent suspension moves out of libc into nxtask_start_fork(), released from nxsched_release_tcb() by nxtask_resume_vfork(). Two things follow: the parent is resumed at exec(), since exec_swap() has already handed the child's pid to the loaded program by the time the vfork stub exits, and vfork() no longer depends on CONFIG_SCHED_WAITPID. Releasing there requires one fix in nxtask_exit(). It raises rtcb->lockcount directly rather than through sched_lock() while it tears the TCB down, so the nxsem_post() that wakes the vfork() parent leaves it queued where a blocked task collects while pre-emption is off -- g_pendingtasks, or g_readytorun on SMP -- and the matching raw lockcount-- does not publish it the way sched_unlock() would, leaving the parent stranded with nothing to move it on. The fix mirrors sched_unlock() for each case: nxsched_merge_pending(), or nxsched_deliver_task() under CONFIG_SMP. Both are no-ops while pre-emption is still disabled, and up_exit() re-reads this_task() afterwards, so a change of the ready-to-run head is honoured. Without it vfork() deadlocks wherever no other task happens to call sched_unlock() afterwards -- rv-virt:nsh64 and rv-virt:pnsh64, where NSH is blocked in waitpid() holding the lock, and qemu-armv8a:citest_smp, which hangs the moment the vfork() test runs. fork() is built on a new addrenv_fork(), backed by an up_addrenv_fork() hook that duplicates an address environment into freshly allocated pages mapped at the same virtual addresses -- unlike up_addrenv_clone(), which copies only the representation and leaves both pointing at the same page tables. The child then adopts the parent's stack geometry rather than being given a relocated copy: a pointer to a stack local taken before fork() must name the same object in the child that it named in the parent, and the parent's stack is already in the duplicate, with its contents, at the parent's address. No architecture implements up_addrenv_fork() yet, so this commit leaves fork() unavailable everywhere. That is the intended state. It withdraws fork() from ARCH_ARM, flat ARCH_ARM64, ARCH_RISCV, ARCH_SIM and ARCH_X86_64, where until now it named the sharing primitive; per-architecture patches restore it, with POSIX semantics, as up_addrenv_fork() lands. In the meantime the sharing primitive is still there under the name that describes it: vfork() for a child that runs a program, pthread_create() for a second flow of control that shares memory, posix_spawn() for both at once. Kconfig: ARCH_HAVE_VFORK inherits ARCH_HAVE_FORK's select lines, conditions included, so no configuration gains machinery; ARCH_HAVE_FORK is redefined to mean "can provide POSIX fork() semantics" and now depends on ARCH_ADDRENV. There is one deliberate departure from "verbatim". ARCH_ARM selected the fork family unconditionally, BUILD_KERNEL included, and that has never worked: on a kernel build the architecture's fork entry point sees the kernel's return address and stack pointer rather than the caller's, so the child resumes at a kernel address. On qemu-armv7a:knsh master faults in ostest's fork case with "Child did not run" and then a data abort; without the condition this change faults the same way through vfork(). ARCH_ARM64 and ARCH_X86_64 already carried "if !BUILD_KERNEL" for exactly this reason -- ARM was the outlier. Conditioning it turns a runtime fault into an honest absence, which is the whole point of the change; arch/arm takes the condition off again in the patch that adds its saved-syscall-frame path. Only the MMU-capable ARM ports are affected, since Cortex-M cannot build BUILD_KERNEL at all. Also fixes two latent syntax errors found on the way: a missing comma in riscv_fork.c and mips_fork.c, both in *_FRAMEPOINTER && !SAVE_GP branches that are never compiled today. BREAKING CHANGE: fork() is withdrawn from every architecture. It is no longer declared in unistd.h, so code that calls it fails to build with an error naming the function, and the sharing behaviour it used to have is gone rather than renamed. CONFIG_ARCH_HAVE_FORK no longer means "fork() exists"; it means "this configuration can provide POSIX fork() semantics", and no architecture selects it yet. Quick fix, chosen by why the call was made: to run a program vfork() + exec*(), or better posix_spawn() a second flow of control that pthread_create() shares the caller's memory a genuinely independent copy keep fork(), and wait for the per-arch patch of the process that implements up_addrenv_fork() and selects CONFIG_ARCH_HAVE_FORK Out-of-tree code that tests CONFIG_ARCH_HAVE_FORK to decide whether a fork-then-exec path is available wants CONFIG_ARCH_HAVE_VFORK instead, which is selected in exactly the places CONFIG_ARCH_HAVE_FORK used to be. The full migration guide is Documentation/guides/fork_vfork_migration.rst. 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>
df3ccde to
9093ae5
Compare
| */ | ||
|
|
||
| mov r0, sp | ||
| mov r1, sp |
There was a problem hiding this comment.
remove since r1 already equals sp
| #ifdef CONFIG_ARCH_HAVE_VFORK | ||
| struct vfork_s | ||
| { | ||
| sem_t sem; /* Posted when the child is done */ |
There was a problem hiding this comment.
wy not use sem_t directly and remove vfork_s
| .globl up_fork | ||
| .type up_fork, @function | ||
| up_fork: | ||
| push {r4, lr} |
There was a problem hiding this comment.
remove extra spaces before {r4, lr}
| mov r4, r0 /* Callee-saved: carries the vfork flag | ||
| * across setjmp() */ |
There was a problem hiding this comment.
merge to previous line
| * stack usage should be the difference between those two. | ||
| */ | ||
|
|
||
| stacktop = (uint64_t)parent->stack_base_ptr + |
There was a problem hiding this comment.
why not check (child->stack_base_ptr == parent->stack_base_ptr) like arm
|
|
||
| SYMBOL(up_fork): | ||
| push %ebx /* Callee-saved: carries the vfork flag | ||
| * across setjmp() */ |
Summary
NuttX implements
fork()andvfork()as the same function. Both are libc wrappers around oneup_fork()syscall;vfork()differs only by a trailingwaitpid(). Underneath, the child joins the parent's address environment — the sameaddrenv_join()thatpthread_create()uses — and gets a private copy of the stack. So the child shares.data,.bssand the heap with its parent and runs concurrently with it.That is not
fork(). It isvfork()-with-a-private-stack underfork()'s name, and the history says so: today'sfork()is NuttX's oldvfork(), renamed in c33d1c9 (2023) without any change of behaviour. The failure was silent — a program written against POSIXfork()compiled, ran, and had its child's writes land in the parent's variables.This is step 1 of the plan agreed in #19540 (ordering, go-ahead): the core semantics, plus the
arch/Kconfigchange that withdrawsfork()from every architecture so per-architecture patches can restore it, one at a time, with real POSIX semantics.What changes
Two primitives, chosen by which function the caller called rather than by what the hardware happens to be.
fork()CONFIG_ARCH_HAVE_FORK, which nowdepends on ARCH_ADDRENVand which no architecture selects yetvfork()_exit()s orexec()sCONFIG_ARCH_HAVE_VFORK, selected exactly whereARCH_HAVE_FORKused to beBelow libc there is still one syscall.
up_fork()gains aboolsaying which primitive the caller used, since the per-architecture register snapshot is the same for both, and passes it tonxtask_setup_fork()— the single place the memory semantics are decided. The flag arrives in the first argument register and is handed on unchanged, so each architecture's entry point only has to keep it alive across its snapshot sequence.The
vfork()parent suspension moves out of libc intonxtask_start_fork(), released fromnxsched_release_tcb(). The parent is therefore resumed atexec(), sinceexec_swap()has already handed the child's pid to the loaded program by the time thevforkstub exits, andvfork()no longer depends onCONFIG_SCHED_WAITPID.fork()is built on a newaddrenv_fork(), backed by anup_addrenv_fork()hook that duplicates an address environment into freshly allocated pages mapped at the same virtual addresses.Breaking change
fork()is withdrawn from every architecture. It is no longer declared inunistd.h, so code that calls it fails to build with an error naming the function, and the sharing behaviour it used to have is gone rather than renamed. A build error is strictly better than the silent wrongness it replaces.Quick fix, chosen by why the call was made:
fork()vfork()+exec*(), or betterposix_spawn()pthread_create()fork(), and wait for the per-architecture patch that implementsup_addrenv_fork()Out-of-tree code that tests
CONFIG_ARCH_HAVE_FORKto decide whether a fork-then-exec path is available wantsCONFIG_ARCH_HAVE_VFORKinstead. The migration guide isDocumentation/guides/fork_vfork_migration.rst.Review
This revision addresses all of @xiaoxiang781216's comments.
task_fork()is gone entirely, along withARCH_HAVE_TASK_FORK,TASK_FORK,FORK_IS_TASK_FORKandARCH_HAVE_ADDRENV_FORK. There is one entry point, still calledup_fork, taking aboolrather than aFORK_TYPE_*selector, with r0 the flag and r1 the context pointer as suggested — which removed the per-primitive entry points and the shared-snapshot trampolines, so the assembly is now smaller than it was before this PR.include/nuttx/fork.his deleted,nxtask_start_vfork()is folded intonxtask_start_fork(), andnxtask_vfork_resume()is renamednxtask_resume_vfork().The companion apache/nuttx-apps#3685 splits
ostest's fork test in two and must merge after this PR. It is not a blocker for CI here: everything inappsthat calledfork()unconditionally was fixed by apache/nuttx-apps#3673, which merged on 2026-08-03, soappsmaster builds against this branch as it stands. Between this PR merging and #3685 merging,ostesthas no fork test — the deliberate cost of carrying no compatibility layer.Testing
Host: macOS 15 (Darwin 25.5.0) on Apple Silicon. QEMU 11.0.3, xPack
riscv-none-elf-gcc14.2.0-3, Arm GNUarm-none-eabi-gcc/aarch64-none-elf-gcc14.2.rel1,xtensa-esp32s3-elf-gcc12.2.0.tools/checkpatch.sh -c -u -m -g— ✔️ all checks pass. Also clean with-b, so it is ready for thebreaking changelabel.Built and run with apache/nuttx-apps#3685,
ostestdriven from NSH:arm/gnu/fork.S+arm_fork.cvfork()passesqemu-armv7a:nsharm/gnu/fork.Svfork()passesqemu-armv8a:nsharm64_fork_func.S+arm64_fork.cvfork()passesqemu-armv8a:citest_smp— SMP, 4 CPUsvfork()passesrv-virt:nsh64risc-v/fork.S+riscv_fork.cvfork()passessim:nshsim_fork_arm64.S+sim_fork.cvfork()passesstm32f4discovery:nsharm/gnu/fork.S-Werror;up_forkdisassembly verifiedrv-virt:pnsh64,rv-virt:knsh64qemu-intel64:nshx86_64/fork.SThe protected and kernel builds generate the right syscall glue for the new argument:
The SMP case earned its row.
nxtask_exit()raisesrtcb->lockcountdirectly rather than throughsched_lock(), so thenxsem_post()that wakes thevfork()parent leaves it queued where a task collects while pre-emption is off, and the matching raw decrement does not publish it. An earlier revision handled that only for!CONFIG_SMP, on the mistaken reasoning that SMP has no pending list — it does not, but the task collects ing_readytorunthere instead and still needs publishing.qemu-armv8a:citest_smphung the instant thevfork()test ran. The fix mirrors whatsched_unlock()already does for each case:nxsched_merge_pending(), ornxsched_deliver_task()underCONFIG_SMP.Three caveats, all independent of this change and all verified against an unmodified
nuttx+ unmodifiedappsbaseline:ostestdoes not run to completion on any target. It aborts intimedmutex_timeout_regression_test()attimedmutex.c:185, added byappsmaster eea8384ff. This is why the fork tests were moved to the top ofuser_main()— otherwise they never run at all.rv-virt:pnsh64does not boot here, at baseline too, so the protected build is verified by compilation and by the generated stubs rather than at runtime.rv-virt:knsh64produces no console output at all in my setup.qemu-intel64cannot run on an Apple Silicon host: NuttX needstsc-deadlineandpcid, which TCG does not implement, and there is no KVM.Please try this on a board you have. CI builds; it does not run
fork()on your hardware. This touches the lowest-level machinery in the system — address environments, stack setup, the architecture's register context — and I would sooner it sat open collecting evidence than merged on the strength of my matrix. Check out this branch together with apache/nuttx-apps#3685, runostest, and thevfork()test is the first thing you will see.