testing/ostest: split the fork test into task_fork, vfork and fork - #3685
Draft
casaroli wants to merge 3 commits into
Draft
testing/ostest: split the fork test into task_fork, vfork and fork#3685casaroli wants to merge 3 commits into
casaroli wants to merge 3 commits into
Conversation
This was referenced Jul 31, 2026
casaroli
force-pushed
the
fork-semantics-ostest-cleanup
branch
from
August 2, 2026 10:35
d3b7722 to
bbc21fe
Compare
acassis
previously approved these changes
Aug 2, 2026
casaroli
force-pushed
the
fork-semantics-ostest-cleanup
branch
from
August 2, 2026 13:57
bbc21fe to
45b4ba7
Compare
| else | ||
| @echo "export ac_cv_func_fork=\"no\"" >> $@ | ||
| endif | ||
| ifneq ($(CONFIG_ARCH_HAVE_VFORK),) |
|
|
||
| /* Define to 1 if you have the `fork' function. */ | ||
|
|
||
| #ifdef CONFIG_ARCH_HAVE_FORK |
| assert_int_equal(open_count, close_count); | ||
| } | ||
|
|
||
| #ifdef CONFIG_ARCH_HAVE_VFORK |
casaroli
force-pushed
the
fork-semantics-ostest-cleanup
branch
from
August 2, 2026 16:51
45b4ba7 to
ce27719
Compare
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>
casaroli
force-pushed
the
fork-semantics-ostest-cleanup
branch
from
August 2, 2026 17:19
ce27719 to
1f61625
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Depends-On: apache/nuttx#19562
Summary
NuttX implements
fork()andvfork()as the same function, and apache/nuttx#19562 splits them into three separate primitives:task_fork()(shares memory, private stack copy, both running),vfork()(shares memory, parent suspended until_exit()/exec()) and POSIXfork()(child gets its own copy). This gives each one a test of its own.Companion PR: apache/nuttx#19562, declared above with
Depends-On:so CI builds the combined change rather than each half against a master that does not yet contain the other. This PR must merge after it, because the symbols each test keys on do not exist until it lands. Theappschange that unblocks that PR's CI is #3673, which merges first and is independent of this one.ostest's "vfork" test was never testingvfork(). It has the child write a global and the parent observe the write — the defining property of sharing, not ofvfork(), whose defining property is that the parent is suspended and whose contract forbids the child to write anything at all. It is renamed totask_fork.c, unchanged, because that is the primitive it has always described. It is also the clearest single piece of evidence for the proposal: the test upstream has run for years is atask_fork()test wearingvfork()'s name.vfork.cis rewritten to test whatvfork()promises. The child does only what POSIX permits — it calls_exit(42)and nothing else, not evenexit(), which would runatexithandlers 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 reachedwaitpid()while the child was still alive. Where child status is not retained —ostest_main()setsSA_NOCLDWAITfor the whole run, deliberately —ECHILDis accepted as equally good evidence, since it says the child was already gone when the parent asked.fork.cis new and tests POSIXfork(): the child's writes to.data,.bssand 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 avfork()child may not — callsmalloc()andprintf(), and returns from the function that calledfork().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. There is no compatibility layer and no mapping between symbols.
task_fork_test()keys onCONFIG_TASK_FORKrather than on the capability symbol:ARCH_HAVE_TASK_FORKsays the architecture can clone a task whileTASK_FORKsays the build asked for it, andtask_fork()is declared only under the latter, so gating on the capability alone would fail to compile aTASK_FORK=nbuild.vfork_test()andfork_test()have no such split and key onARCH_HAVE_VFORKandARCH_HAVE_FORKdirectly.The other in-tree callers are audited for which primitive they actually meant:
interpreters/python's_posixsubprocessandnetutils/libwebsockets'LWS_HAVE_WORKING_VFORKwant the fork-then-exec path —ARCH_HAVE_VFORK.python'sos.fork()andlibwebsockets'LWS_HAVE_FORKmean realfork()and stay onARCH_HAVE_FORK, so they become absent rather than silently wrong.testing/fs/fdsantest'svforkcase followsARCH_HAVE_VFORK.Impact
Between #3673 merging and apache/nuttx#19562 merging,
ostesthas no fork test. That is the deliberate cost of carrying no compatibility layer:CONFIG_TASK_FORKandCONFIG_ARCH_HAVE_VFORKdo not exist on a pre-split NuttX, sotask_fork_test()andvfork_test()are not built, andfork_test()is not built either because after the splitARCH_HAVE_FORKis off until a per-architecture PR turns it on. Coverage returns the moment the NuttX side lands.fork_test()costs nothing on size-constrained configurations, because it is not built on them. It keys onARCH_HAVE_FORK, which no architecture sets untilup_addrenv_fork()lands for it, solm3s6965-ek:qemu-protected— the configuration with the tightest user flash region — never compiles it.fork_test()itself is exercised by the per-architecture PRs that follow, which are what turnARCH_HAVE_FORKback on.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-gcc14.2.Rel1.Style
../nuttx/tools/checkpatch.sh -c -u -m -g <base>..HEAD, the exact command.github/workflows/check.ymlruns — ✔️ All checks pass, withcodespell,cvt2utf,cmake-formatandnxstyleall installed.Verification
This PR was restructured after review: the compatibility layer is gone entirely, each test keys on the primitive it tests, and
Depends-On:now carries the relationship to apache/nuttx#19562 that the fallbacks used to stand in for. The branch has been rebased onto current master.The functional matrix is being re-run against the restructured branch and will be posted here — full
ostestto exit status 0 onrv-virt:nsh64,rv-virt:pnsh64,rv-virt:knsh64,qemu-armv7a:nsh,qemu-armv8a:nsh,qemu-intel64:nshandsim:ostest, pluslm3s6965-ek:qemu-protectedfree bytes against theappsmaster baseline. The previous revision's results are in this PR's history.