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]

Reply via email to