casaroli opened a new pull request, #3673: URL: https://github.com/apache/nuttx-apps/pull/3673
*Note: Please adhere to [Contributing Guidelines](https://github.com/apache/nuttx/blob/master/CONTRIBUTING.md).* ## Summary This is the `apps` half of apache/nuttx#19540, and it **lands first**. NuttX implements `fork()` and `vfork()` as the same function, and is gaining the three separate primitives that issue describes: `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 PR is written to work against NuttX **with or without** that change, so the tests keep running across the transition instead of silently compiling out. **Companion PR: apache/nuttx#TBD.** **`ostest`'s "vfork" test was never testing `vfork()`.** It has the child write a global and the parent observe the write — which is 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. It is the clearest single piece of evidence for the whole proposal: the test upstream has been running for years is a `task_fork()` test wearing `vfork()`'s name, sitting under a `CONFIG_ARCH_HAVE_FORK` guard. **`vfork.c` is rewritten** to test what `vfork()` actually 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*; that is exactly the misuse `vfork()`'s restrictions exist to prevent, and a test that did it would be testing the wrong thing. Because 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()`. **All three run at the top of `user_main()`** rather than in the middle. 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. Finding that out in seconds rather than after everything else has passed is the difference between a usable iteration and a coffee break when a port is being brought up. **The other in-tree callers are audited** for which primitive they actually meant: * `testing/drivers/nand_sim` wants a daemon that outlives its caller and shares its memory — `task_fork()`. * `interpreters/bas`'s `SHELL` and `EDIT` statements, `interpreters/python`'s `_posixsubprocess`, and `netutils/libwebsockets`' `LWS_HAVE_WORKING_VFORK` want the fork-then-exec path — `vfork()`. * `python`'s `os.fork()` (`ac_cv_func_fork`) and `libwebsockets`' `LWS_HAVE_FORK` / `LWS_HAVE_WORKING_FORK` mean real `fork()` and stay on `CONFIG_ARCH_HAVE_FORK` — which, correctly, is how they now become *absent* rather than silently wrong. * `testing/fs/fdsantest`'s `vfork` case follows `vfork()`. ## Impact **Merged on its own, against today's NuttX, nothing regresses.** `CONFIG_ARCH_HAVE_TASK_FORK` and `CONFIG_ARCH_HAVE_VFORK` do not exist yet, so everything here also accepts the `CONFIG_ARCH_HAVE_FORK` that stands in for them: today's `fork()` *is* `task_fork()`, and today's `vfork()` is that plus a `waitpid()`. `ostest` therefore keeps building and passing both `task_fork_test` and `vfork_test` exactly as it builds and passes the current `vfork` test. `fork_test()` deliberately has **no** such fallback. The copy semantics it checks are precisely what today's `fork()` does not provide, so it is gated on `ARCH_HAVE_VFORK`, whose existence is the evidence that the split has landed. Against today's NuttX it is simply not built. The compatibility layer is three `#define`s in `ostest.h` (`OSTEST_HAVE_TASK_FORK` / `_VFORK` / `_FORK`), a `task_fork() -> fork()` shim, and `|| ARCH_HAVE_FORK` on four Kconfig/preprocessor guards elsewhere. A small follow-up removes all of it once the NuttX side is in; that follow-up **must not** merge before the NuttX PR. `CONFIG_TESTING_NAND_SIM` gains a dependency on `ARCH_HAVE_TASK_FORK || ARCH_HAVE_FORK`. It called `fork()` unconditionally before and would not have linked on a target without it. ## Testing Host: macOS 15 (Darwin 25.5.0) on Apple Silicon. QEMU 11.0.3, xPack `riscv-none-elf-gcc` 14.2.0-3. ### Against unmodified `apache/nuttx` master (`7fd17c9d7c`) — the case this PR must not break `rv-virt:nsh64`, `ostest`: * Config has only `CONFIG_ARCH_HAVE_FORK=y`, as expected — neither new symbol exists. * `nm` on the image shows `task_fork_test` and `vfork_test` built, and **`fork_test` absent**, which is exactly the intent. * Run: `task_fork_test: Child 5 ran successfully`, `vfork_test: Child 6 ran and exited before the parent resumed`, `ostest_main: Exiting with status 0`. ### Against the NuttX PR branch Full `ostest` suite to **exit status 0** on `rv-virt:nsh64` (FLAT), `rv-virt:pnsh64` (PROTECTED), `rv-virt:knsh64` (KERNEL), `qemu-armv7a:nsh`, `qemu-armv8a:nsh` and `qemu-intel64:nsh`, with `task_fork_test` and `vfork_test` passing and `fork_test` correctly absent — no architecture provides POSIX `fork()` at that point in the series. `sim:ostest` also builds and passes both. `fork_test()` itself is verified by the per-architecture PRs that follow, which are what turn `CONFIG_ARCH_HAVE_FORK` back on. It has been run to completion on RISC-V, arm64, armv7-a and x86_64 kernel builds on the development branch those PRs are cut from — `fork_test: Parent and child had independent memory` — so it is not being added untested; it is simply not reachable until the first `up_addrenv_fork()` lands. -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: [email protected] For queries about this service, please contact Infrastructure at: [email protected]
