casaroli opened a new issue, #19540:
URL: https://github.com/apache/nuttx/issues/19540

   ### Is your feature request related to a problem? Please describe.
   
   # Proposal: give `fork()` and `vfork()` their real, separate semantics
   
   > **A note before I start.** My apologies if this has been raised and 
discussed
   > before — I would not be surprised if it has, and I have no wish to 
relitigate
   > a settled decision for its own sake. What brings me to it now is concrete: 
I
   > want NuttX to work well on MMU-capable systems, and while implementing a 
real
   > `fork()` for one I found that the current design makes it impossible to do
   > coherently. That is my motivation for bringing the subject back. If there 
is
   > prior discussion I have missed, I would be glad to be pointed at it.
   
   ## Summary
   
   NuttX today implements `fork()` and `vfork()` as **the same function**. Both 
are
   thin wrappers around a single `up_fork()`, there is no `SYS_vfork` and no
   `up_vfork()`, and the shared implementation gives the child **a copy of the
   parent's stack, sharing everything else**.
   
   That is not `fork()`: the child does not get its own memory — writes by the
   child are visible to the parent, and vice versa. What it is, is `vfork()` 
with
   a private stack: a `vfork()` conforming in its memory semantics (a program
   that honours `vfork()`'s restrictions cannot tell the difference there —
   though the moment the parent resumes is a separate defect, §1.5), but
   published under `fork()`'s name — and under that name, the sharing is simply
   wrong. Nor is this a coincidence: today's `fork()` *is* NuttX's old 
`vfork()`,
   renamed (§1.4).
   
   This proposal is to separate them:
   
   | | semantics | availability |
   |---|---|---|
   | `fork()` | child gets **its own copy** of the parent's memory | only where 
an address environment can be duplicated |
   | `vfork()` | child **shares** the parent's memory, parent suspended until 
`_exit()`/`exec()` | implementable everywhere, MMU or not |
   
   And today's exact behaviour — shared memory, private stack, both running
   concurrently — is not lost: it is retained under an honest, non-POSIX name,
   **`task_fork()`** (§4.4), on the architectures and configurations that 
support
   it today.
   
   This is a **breaking change**. It is proposed anyway because the current
   behaviour is silently wrong for `fork()` users, it is not portable across 
NuttX
   targets, and it is the thing blocking correct `fork()` support on MMU-capable
   systems.
   
   A compatibility option, `CONFIG_FORK_IS_TASK_FORK` (§4.5), aliases `fork()`
   back to `task_fork()`, keeping existing applications building and running
   **exactly** as they do today on every configuration where `fork()` exists
   today.
   
   *All code citations below are quoted verbatim from `apache/nuttx` master at
   
[`216edd74d`](https://github.com/apache/nuttx/tree/216edd74db80d9b1db3c87683e14b03d8b34e279)
   and `apache/nuttx-apps` master at
   
[`6ca1272b9`](https://github.com/apache/nuttx-apps/tree/6ca1272b99494686731c53d22f81224d7ab8117d);
   every file reference links to the code at those commits, so the line numbers
   are stable.*
   
   ---
   
   ## 1. What NuttX does today
   
   ### 1.1 `fork()` and `vfork()` are wrappers around the same call
   
   
[`libs/libc/unistd/lib_fork.c:153-234`](https://github.com/apache/nuttx/blob/216edd74db80d9b1db3c87683e14b03d8b34e279/libs/libc/unistd/lib_fork.c#L153-L234)
   — both entry points call `up_fork()`:
   
   ```c
   pid_t fork(void)
   {
     ...
     pid = up_fork();
     ...
     return pid;
   }
   
   pid_t vfork(void)
   {
     ...
     pid = up_fork();
     ...
     if (pid != 0)
       {
         /* we are in parent task, and we need to wait the child task
          * until running finished or performing exec
          */
   
         ret = waitpid(pid, &status, WNOWAIT);
         ...
       }
   
     return pid;
   }
   ```
   
   `vfork()` differs from `fork()` **only** by the parent's `waitpid()`. The 
memory
   semantics are identical, because the underlying operation is identical. (The
   `WNOWAIT` flag leaves the child unreaped, so the application's own
   `waitpid()` still collects its status afterwards. Note in passing that
   `vfork()` is only compiled under `CONFIG_SCHED_WAITPID`
   
([`lib_fork.c:176`](https://github.com/apache/nuttx/blob/216edd74db80d9b1db3c87683e14b03d8b34e279/libs/libc/unistd/lib_fork.c#L176))
   — a configuration without that option has no `vfork()` at all — and that
   suspending the parent with `waitpid()` is itself subtly wrong; see §1.5.)
   
   There is exactly one syscall
   
([`include/sys/syscall_lookup.h:116`](https://github.com/apache/nuttx/blob/216edd74db80d9b1db3c87683e14b03d8b34e279/include/sys/syscall_lookup.h#L116)):
   
   ```c
   SYSCALL_LOOKUP(up_fork, 0)
   ```
   
   There is no `SYS_vfork` and no `up_vfork()` in the tree. **Below libc, the 
two
   POSIX functions are indistinguishable**, so no implementation can give them
   different behaviour even if it wanted to.
   
   ### 1.2 The child shares the parent's memory
   
   
[`sched/task/task_fork.c:162-174`](https://github.com/apache/nuttx/blob/216edd74db80d9b1db3c87683e14b03d8b34e279/sched/task/task_fork.c#L162-L174):
   
   ```c
   #if defined(CONFIG_ARCH_ADDRENV)
     /* Join the parent address environment */
   
     if (ttype != TCB_FLAG_TTYPE_KERNEL)
       {
         ret = addrenv_join(parent, child);
         ...
       }
   #endif
   ```
   
   `addrenv_join()` is the same call `pthread_create()` uses
   
([`sched/pthread/pthread_create.c:238`](https://github.com/apache/nuttx/blob/216edd74db80d9b1db3c87683e14b03d8b34e279/sched/pthread/pthread_create.c#L238))
   to give a *thread* the memory of its creator. The forked child is therefore
   given the memory relationship of a thread.
   
   The child then gets a **new stack**
   ([`up_create_stack()`, 
`task_fork.c:201`](https://github.com/apache/nuttx/blob/216edd74db80d9b1db3c87683e14b03d8b34e279/sched/task/task_fork.c#L201)),
   into which the architecture's fork code copies the parent's stack. So the 
child
   has a private stack and shares everything else: `.data`, `.bss`, the heap.
   
   On builds without `CONFIG_ARCH_ADDRENV` (flat and protected builds) there is
   only one address space to begin with, so everything except the stack is 
shared
   there too.
   
   ### 1.3 NuttX's own documentation already describes `vfork()`
   
   The most direct evidence that the current function is `vfork()` wearing
   `fork()`'s name is NuttX's own comment on `nxtask_setup_fork()`
   
([`sched/task/task_fork.c:57-62`](https://github.com/apache/nuttx/blob/216edd74db80d9b1db3c87683e14b03d8b34e279/sched/task/task_fork.c#L57-L62)):
   
   > The **fork()** function has the same effect as posix fork(), except that 
the
   > behavior is **undefined** if the process created by fork() either 
**modifies
   > any data** other than a variable of type `pid_t` used to store the return
   > value from fork(), or **returns from the function** in which fork() was
   > called, or **calls any other function** before successfully calling 
`_exit()`
   > or one of the exec family of functions.
   
   Those three restrictions are, word for word, the definition of **`vfork()`** 
in
   POSIX.1-2004. They are not restrictions that `fork()` has, or may have. NuttX
   has documented `fork()` by quoting the specification of `vfork()`.
   
   ### 1.4 How it got this way: `vfork()` was renamed to `fork()`
   
   This is not an accident of implementation. It is recorded in the history.
   
   Commit
   
[**`c33d1c9c97`**](https://github.com/apache/nuttx/commit/c33d1c9c977680c5c1f5214c260269b782fdff09)
   — *"sched/task/fork: add fork implementation"*, guoshichao, 2023-07-05 — 
states
   its own intent:
   
   > 1. as we can use fork to implement vfork, so we **rename the vfork to 
fork**,
   >    and use the fork method as the base to implement vfork method
   > 2. create the vfork function as a libc function based on fork function
   
   The diff is a mass rename. Twenty-two files were renamed rather than written:
   
   ```
   arch/arm/src/common/{arm_vfork.c => arm_fork.c}
   arch/arm/src/common/gnu/{vfork.S => fork.S}
   arch/arm64/src/common/{arm64_vfork.c => arm64_fork.c}
   arch/ceva/src/common/{ceva_vfork.c => ceva_fork.c}
   arch/mips/src/mips32/{vfork.S => fork.S}
   ...
   ```
   
   The Kconfig symbol was renamed too, for every architecture:
   
   ```diff
    config ARCH_ARM
        bool "ARM"
   -    select ARCH_HAVE_VFORK
   +    select ARCH_HAVE_FORK
   ```
   
   and so was the system call:
   
   ```diff
   -#if defined(CONFIG_SCHED_WAITPID) && defined(CONFIG_ARCH_HAVE_VFORK)
   -  SYSCALL_LOOKUP(vfork,                    0)
   +#ifdef CONFIG_ARCH_HAVE_FORK
   +  SYSCALL_LOOKUP(fork,                     0)
   ```
   
   with `libs/libc/unistd/lib_vfork.c` added on top as a new libc wrapper.
   
   So **today's `fork()` is, quite literally, NuttX's old `vfork()` under a new
   name.** No copy semantics were added — the implementation was not changed, 
only
   what it is called.
   
   Follow-ups completed the picture:
   
[`3524f4b9c`](https://github.com/apache/nuttx/commit/3524f4b9ce4e1c12dd30dda7cb9a161406485ee6)
   (*"libs/libc/fork: add lib_fork implementation"*, 2023-07-16) renamed the
   syscall again, to the `up_fork` seen in §1.1;
   
[`79c7962af`](https://github.com/apache/nuttx-apps/commit/79c7962af67dc14a40bc140cb3377e2c8e7b99e0)
   (*"apps: Replace CONFIG_ARCH_HAVE_VFORK with CONFIG_ARCH_HAVE_FORK"*,
   2023-08-13) propagated the rename through `apps`; and
   
[`2f2632338`](https://github.com/apache/nuttx/commit/2f263233884182726283676d972c2b6922619dd3)
   (*"arch/libc: Integrate vfork into fork, and vfork directly call up_fork"*,
   2024-10-09) removed the last remaining distinction below libc, leaving
   `vfork()` as `fork()` plus a `waitpid()` (§1.1).
   
   The premise in the commit message, *"as we can use fork to implement 
vfork"*, is
   sound in the direction it is stated: if you have a real `fork()`, you can 
build
   `vfork()` on it. What happened was the reverse — the existing `vfork()` was 
given
   `fork()`'s name, so NuttX ended up with `vfork()` implementing `fork()`, 
which
   does not work in that direction.
   
   That framing is worth keeping in mind when weighing this proposal: it is not
   asking to change what `fork()` has always meant in NuttX. It is asking to 
undo a
   rename, restore `ARCH_HAVE_VFORK` and `SYS_vfork` to the implementation that
   actually is `vfork()`, and add a genuine `fork()` where one can be 
implemented.
   
   ### 1.5 Even as `vfork()`, the parent is resumed at the wrong moment
   
   POSIX resumes a `vfork()` parent when the child calls `_exit()` **or one of 
the
   exec functions**. NuttX's `vfork()` suspends the parent in `waitpid()` 
(§1.1),
   which returns only when the child *terminates* — nothing in the exec path 
wakes
   it.
   
   Today the exec case appears to work only by accident: NuttX's `execve()` does
   not overlay the calling process. It starts the new program as a **separate 
task
   with a new pid**, and the calling task then exits
   
([`sched/task/task_execve.c:115-138`](https://github.com/apache/nuttx/blob/216edd74db80d9b1db3c87683e14b03d8b34e279/sched/task/task_execve.c#L115-L138)
   — `exec()` followed by `_exit(0)`). It is the child's *exit* that releases 
the
   parent, and the parent is left with no relationship to the program it just
   launched. The file
   [says so 
itself](https://github.com/apache/nuttx/blob/216edd74db80d9b1db3c87683e14b03d8b34e279/sched/task/task_execve.c#L90-L96):
   
   > the most serious of these is that the exec'ed task will not have the same
   > task ID as the vfork'ed function. So the parent function cannot know the ID
   > of the exec'ed task.
   
   So the canonical `vfork()` + `exec()` + `waitpid()` idiom cannot actually be
   written on NuttX: the pid the parent holds names a task that died at exec 
time,
   and the real program runs under a pid the parent cannot learn. Repairing
   `exec()`'s pid discontinuity is beyond this proposal's scope, but the 
`vfork()`
   half — resuming the parent at exec rather than at termination — falls out
   naturally once the suspension lives in the kernel primitive instead of a libc
   `waitpid()` (§4.2, §7).
   
   ---
   
   ## 2. What POSIX says
   
   ### 2.1 `fork()`
   
   [POSIX.1-2017 
`fork()`](https://pubs.opengroup.org/onlinepubs/9699919799/functions/fork.html):
   
   > The `fork()` function shall create a new process. The new process (child
   > process) shall be an **exact copy** of the calling process (parent process)
   > except as detailed below […]
   
   The separation of the two processes' memory is the defining property. It is 
what
   makes the following legal, and it is what essentially all `fork()`-using code
   relies on:
   
   * the child may modify any variable, and the parent will not see it;
   * the parent may modify any variable after the fork, and the child will not 
see
     it;
   * the child may return from the function that called `fork()`;
   * the child may call arbitrary functions — `malloc()`, `printf()`, anything —
     and run indefinitely;
   * both processes may run concurrently.
   
   The first two — the memory guarantees, the defining property — do not hold in
   NuttX today. (The other three do, courtesy of the private stack copy; that
   they hold is exactly what separates today's behaviour from classic `vfork()` 
—
   see §4.4.)
   
   ### 2.2 `vfork()`
   
   [POSIX.1-2004 (SUSv3) 
`vfork()`](https://pubs.opengroup.org/onlinepubs/009695399/functions/vfork.html):
   
   > The `vfork()` function shall be equivalent to `fork()`, except that the
   > behavior is **undefined** if the process created by `vfork()` either 
modifies
   > any data other than a variable of type `pid_t` used to store the return 
value
   > from `vfork()`, or returns from the function in which `vfork()` was 
called, or
   > calls any other function before successfully calling `_exit()` or one of 
the
   > exec family of functions.
   
   and, on why it exists at all:
   
   > The `vfork()` function differs from `fork()` only in that the child process
   > **can share code and data with the calling process** (parent process). This
   > speeds cloning activity significantly at a risk to the integrity of the 
parent
   > process if `vfork()` is misused.
   
   Two things follow.
   
   **First, sharing is `vfork()`'s entire purpose.** It exists to avoid the 
copy.
   An implementation of `vfork()` that copies is conforming but pointless — and 
on
   a system without an MMU, an implementation that copies is not possible at 
all.
   `vfork()` is precisely the primitive that a no-MMU system *can* offer.
   
   **Second, the restrictions are the price of that sharing.** They are not
   arbitrary; they exist because the child is running in the parent's address 
space
   on borrowed time. Applying those restrictions to `fork()` — as NuttX's
   documentation does — inverts the relationship: it takes `vfork()`'s cost and
   attaches it to `fork()`'s name, while delivering neither `fork()`'s guarantee
   nor `vfork()`'s performance benefit.
   
   `vfork()` was marked obsolescent in POSIX.1-2001 and **removed** in 
POSIX.1-2008.
   That is an argument for not *requiring* it, not an argument for redefining 
it:
   where it is provided, it should mean what it has always meant. It remains 
widely
   implemented (Linux, the BSDs) for exactly the reason POSIX gives — it is the
   cheap clone.
   
   ### 2.3 On "`vfork()` may be implemented as `fork()`"
   
   It is often said, correctly, that a conforming implementation may implement
   `vfork()` as `fork()`, because a program that depends on the sharing is 
invoking
   undefined behaviour. This is true and it is not the situation NuttX is in.
   
   NuttX has done the **opposite**: it implements `fork()` as `vfork()`. That
   direction is not defensible under any reading of the standard. A program that
   depends on `fork()`'s copy semantics is not invoking undefined behaviour — 
it is
   using the function exactly as specified.
   
   ---
   
   ## 3. Why this matters
   
   ### 3.1 The failure is silent
   
   A program that uses `fork()` correctly — as specified, as it works on Linux, 
on
   the BSDs, on macOS — **compiles and runs on NuttX and produces wrong 
results**.
   There is no compiler diagnostic, no link error, no runtime error. The child's
   writes quietly land in the parent's variables.
   
   Silent wrongness is the worst possible failure mode. A link error would be
   strictly better.
   
   ### 3.2 Availability varies across NuttX targets; the semantics are wrong 
everywhere
   
   `CONFIG_ARCH_HAVE_FORK` is selected inconsistently in `arch/Kconfig`:
   
   * [`ARCH_ARM` — `select 
ARCH_HAVE_FORK`](https://github.com/apache/nuttx/blob/216edd74db80d9b1db3c87683e14b03d8b34e279/arch/Kconfig#L14)
   * [`ARCH_ARM64` — `select ARCH_HAVE_FORK if !BUILD_KERNEL && 
!BUILD_PROTECTED`](https://github.com/apache/nuttx/blob/216edd74db80d9b1db3c87683e14b03d8b34e279/arch/Kconfig#L32)
   * [`ARCH_RISCV` — `select 
ARCH_HAVE_FORK`](https://github.com/apache/nuttx/blob/216edd74db80d9b1db3c87683e14b03d8b34e279/arch/Kconfig#L90)
   * [`ARCH_SIM` — `select ARCH_HAVE_FORK if 
!HOST_WINDOWS`](https://github.com/apache/nuttx/blob/216edd74db80d9b1db3c87683e14b03d8b34e279/arch/Kconfig#L114)
   * [`ARCH_X86_64` — `select ARCH_HAVE_FORK if 
!BUILD_KERNEL`](https://github.com/apache/nuttx/blob/216edd74db80d9b1db3c87683e14b03d8b34e279/arch/Kconfig#L150)
   
   So whether `fork()` exists at all depends on the architecture *and* the build
   model: present on ARM in every build model, absent from ARM64 kernel and
   protected builds and from x86_64 kernel builds, present on the Linux/macOS
   simulator, absent on the Windows one. An in-tree application can test
   `CONFIG_ARCH_HAVE_FORK`, but that is a NuttX-specific config symbol, and it
   reports only that `fork()` *exists* — never that its semantics are not
   POSIX's. **There is no standard mechanism** — no feature-test macro, no
   `sysconf()` query — by which portable code can discover any of this. It 
cannot
   even degrade gracefully: it links on one target and fails to link on the 
next.
   
   And where `fork()` does exist, it always means "share". Even the simulator,
   which runs on a host with a perfectly good `fork(2)`, does not use it:
   
[`arch/sim/src/sim/sim_fork.c`](https://github.com/apache/nuttx/blob/216edd74db80d9b1db3c87683e14b03d8b34e279/arch/sim/src/sim/sim_fork.c#L91)
   performs the same snapshot-the-registers, copy-the-stack emulation as the
   embedded targets — its own comment calls the stack copy a
   [*"feeble effort to preserve the stack 
contents"*](https://github.com/apache/nuttx/blob/216edd74db80d9b1db3c87683e14b03d8b34e279/arch/sim/src/sim/sim_fork.c#L131-L136).
   **There is no NuttX target on which `fork()` copies.**
   
   ### 3.3 It blocks correct MMU support
   
   This is what prompted the proposal. On a target with an MMU and address
   environments, a real `fork()` is implementable: allocate the child its own 
pages,
   copy the parent's regions into them, map them at the same virtual addresses.
   
   There is currently no way to express that, because `fork()` and `vfork()` 
are one
   code path reached through one syscall. Implementing real copy semantics
   necessarily changes `vfork()` too — and breaks it, since `vfork()`'s whole
   contract is the sharing that was just removed.
   
   We hit exactly this. On an ESP32-S3 `BUILD_KERNEL` port we implemented a real
   copying `fork()` (child gets its own physical pages at the same virtual
   addresses; verified on hardware — the child's writes are invisible to the
   parent). The first thing it broke was **`ostest`'s `vfork` test**
   
([`apps/testing/ostest/vfork.c:44-74`](https://github.com/apache/nuttx-apps/blob/6ca1272b99494686731c53d22f81224d7ab8117d/testing/ostest/vfork.c#L44-L74)),
   which has the child set a global and the parent observe it:
   
   ```c
   static volatile bool g_vforkchild;
   
     g_vforkchild = false;
     pid = vfork();
     if (pid == 0)
       {
         g_vforkchild = true;
         exit(0);
       }
     ...
     sleep(1);
     if (g_vforkchild) /* parent expects to see the child's write */
   ```
   
   That test is correct — it is verifying the property that makes `vfork()`
   `vfork()`. It broke because `fork()` and `vfork()` are the same function, so
   fixing one necessarily breaks the other. **That is the conflation, 
demonstrated.**
   
   ---
   
   ### Describe the solution you'd like
   
   ## 4. Proposal
   
   ### 4.1 `fork()`
   
   * Child receives its **own copy** of the parent's memory, at the same virtual
     addresses.
   * No restrictions beyond POSIX's: the child may modify anything, call 
anything,
     return from the calling function, and run concurrently with the parent.
   * Available **only** where the architecture and build model can duplicate an
     address environment.
   * Where it cannot be provided, `fork()` **is not provided at all** — the 
symbol
     does not exist and applications that call it **fail to link**.
   
   One cost is inherent and worth stating up front: without a paging MMU there 
is
   no copy-on-write, so this `fork()` copies the parent's memory eagerly —
   forking a large process on a small-RAM target is expensive and can fail with
   `ENOMEM`. That is the nature of the primitive, not a defect of an
   implementation: `fork()`'s value here is correctness for portable code.
   Spawn-heavy code should prefer `posix_spawn()` or `vfork()`, on NuttX as
   anywhere.
   
   The link failure is a feature. It converts today's silent runtime wrongness 
into
   a build-time error naming the exact function that cannot be honoured, which 
the
   developer can then replace with `vfork()`, `task_fork()` (§4.4),
   `posix_spawn()` or `task_spawn()`.
   
   `CONFIG_ARCH_HAVE_FORK` should be redefined to mean *"this configuration can
   provide POSIX `fork()` semantics"*: it requires `CONFIG_ARCH_ADDRENV` plus a
   new `CONFIG_ARCH_HAVE_ADDRENV_FORK`, selected by architectures that can
   duplicate an address environment. The existing unconditional selects become
   conditional.
   
   ### 4.2 `vfork()`
   
   * Child **shares** the parent's memory. No copy.
   * Parent is suspended until the child calls `_exit()` or one of the `exec()`
     family — and is resumed at `exec()`, not merely at termination (§1.5). That
     requires the suspension to live in the kernel primitive rather than in a
     libc `waitpid()`, which also frees `vfork()` of its current
     `CONFIG_SCHED_WAITPID` dependency.
   * Restrictions are POSIX's: the child must not modify data other than the
     `pid_t`, must not return from the calling function, and must not call other
     functions before `_exit()`/`exec()`. Programs that violate them get 
undefined
     behaviour, as they always have.
   * **Implementable everywhere**, MMU or not. This is the primitive a no-MMU 
system
     can implement, and it should not silently gain a copy on systems that have 
an
     MMU — `vfork()` means "do not copy", and that is why callers choose it.
   * Governed by `CONFIG_ARCH_HAVE_VFORK`.
   
   A word on the stack. Classic `vfork()` (BSD, Linux) has the child **borrow
   the parent's actual stack** — total borrowing, no allocation, no copy — and
   the parent's suspension is precisely what makes that safe. NuttX instead 
gives
   the child a *copy* of the parent's stack at a different address. That too is 
a
   workaround, not a design choice: the shared primitive never suspends the
   parent, and two simultaneously runnable tasks cannot share one stack.
   
   On ARM and RISC-V the relocated copy works in practice, because frame
   addressing is stack-pointer-relative and nothing in the return path consumes
   absolute stack addresses. On Xtensa's **windowed ABI it cannot work at all**:
   the register-window base save areas embedded in the stack hold **absolute**
   stack-pointer values (the spilled `a1` of each frame), and the window
   underflow machinery restores them as-is. A child started on a relocated copy
   takes its very first window underflow on the `retw` that returns from libc's
   `vfork()` — before the application ever sees `pid == 0` — reloads a stack
   pointer that points into the *parent's* stack, and from then on runs on the
   parent's live frames, corrupting them. Nor can the copy be fixed up in
   general: the save-area chain could be rebased, but pointers into the stack 
can
   live in any register or variable. This is very likely why Xtensa has never
   selected `ARCH_HAVE_FORK`.
   
   The split resolves this on both sides. A true `vfork()` child borrows the
   parent's stack, so there is nothing to relocate — a windowed ABI is satisfied
   by construction, and `vfork()` gains its full economy: no second stack, no
   copy. A true `fork()` duplicates the address environment **at the same 
virtual
   addresses**, so the absolute pointers in the child's copied stack — window
   save areas included — remain valid without any fixup. The one variant a
   windowed ABI cannot support is exactly the hybrid NuttX has today: shared
   memory with a relocated stack copy. On such architectures the borrow is
   therefore *required*; on the others it is the natural optimization.
   
   ### 4.3 Separate them below libc
   
   Add `SYS_vfork` / `up_vfork()` alongside `up_fork()`, so the semantics are 
chosen
   by **which function the application called** and never by what the hardware
   happens to be. This is the change that makes the rest expressible.
   
   ### 4.4 `task_fork()` — today's behaviour under an honest name
   
   Everything above removes the *name* `fork()` from the sharing behaviour; it
   does not remove the behaviour itself. What NuttX builds today — child shares
   `.data`/`.bss`/heap, runs on a **private copy** of the parent's stack, and
   executes **concurrently** with the parent — is a coherent, useful primitive.
   It is not `fork()` and not `vfork()`; it is a *task* cloned at the call site,
   with the memory relationship of a thread (§1.2). Precedent exists: it is
   exactly Plan 9's `rfork(RFPROC|RFMEM)` — data and bss shared, stack copied —
   and, more loosely, Linux's `clone(CLONE_VM)` without `CLONE_VFORK`, whose
   child likewise shares memory and runs concurrently, though on a
   caller-supplied stack rather than a copy.
   
   The proposal is to keep it, exposed as a non-POSIX NuttX API named for what 
it
   is:
   
   * **`task_fork()`** — joins the existing `task_create()` / `task_spawn()` /
     `task_delete()` family. Returns twice, like `fork()`: 0 in the child, the
     child's pid in the parent. No suspension, no memory copy beyond the stack.
   * Governed by **`CONFIG_ARCH_HAVE_TASK_FORK`**, which inherits **exactly
     today's `ARCH_HAVE_FORK` select lines** — the machinery is unchanged, only
     the name is honest. In particular it is *never* available on windowed-ABI
     Xtensa, in any build model, for the reasons in §4.2 — which today's selects
     already reflect.
   * The name deliberately avoids "vfork": the defining property of `vfork()` is
     the suspension, which this primitive does not have. A name like
     `stack_vfork()` would recreate the very confusion this proposal removes.
   
   Two things this buys:
   
   1. **Exact legacy compatibility.** `CONFIG_FORK_IS_TASK_FORK` (§4.5) aliases
      `fork()` to `task_fork()` and legacy applications are *bit-for-bit* where
      they are today — same sharing, same concurrency, no new suspension.
   2. **A migration target that is a rename, not a rewrite.** In-tree code found
      by the audit (§5, item 3) to depend on shared-memory concurrency changes
      one identifier and is done.
   
   New code should prefer `pthread_create()` (the honest concurrent-sharing
   primitive) or `posix_spawn()`; `task_fork()` exists to give the historical
   behaviour a truthful name, not to recommend it.
   
   ### 4.5 Compatibility: `CONFIG_FORK_IS_TASK_FORK`
   
   For existing code that calls `fork()` and expects today's behaviour:
   
   ```
   config FORK_IS_TASK_FORK
        bool "Provide fork() as an alias for task_fork() (legacy)"
        default n
        depends on ARCH_HAVE_TASK_FORK && !ARCH_HAVE_FORK
        ---help---
                Provide fork() on configurations that cannot implement POSIX 
fork()
                semantics, by aliasing it to task_fork().
   
                This preserves the historical NuttX fork() behaviour exactly: 
the
                child shares the parent's data and heap, runs on a private copy 
of
                the parent's stack, and runs concurrently with the parent.  It 
does
                NOT provide POSIX fork() semantics: writes by the child are 
visible
                to the parent, and vice versa.
   
                Enable this only to keep legacy applications building.  New code
                should call task_fork() explicitly, or use pthread_create() or
                posix_spawn().
   ```
   
   Because the alias targets `task_fork()`, it is **exact**: same sharing, same
   concurrency, no new suspension. And because `ARCH_HAVE_TASK_FORK` inherits
   today's `ARCH_HAVE_FORK` select lines (§4.4), at the time the change lands 
the
   option exists on precisely the configurations where `fork()` exists today — 
no
   configuration that can call `fork()` now loses the ability to keep it, and
   configurations that never had `fork()` lose nothing. As real `fork()` support
   arrives per architecture that stops being true for the configurations gaining
   it — deliberately; see the note on `!ARCH_HAVE_FORK` below.
   
   Default `n`, so the honest behaviour is what you get unless you ask 
otherwise,
   and the Kconfig help states plainly what you are opting into. Existing users
   flip one switch and are exactly where they are today.
   
   The `depends on !ARCH_HAVE_FORK` is deliberate: on a configuration that
   provides real `fork()`, code that wants the sharing has honest spellings —
   `task_fork()` and `vfork()` — and aliasing `fork()` back to sharing there
   would reintroduce exactly the ambiguity this proposal removes. On such
   targets, sharing-dependent callers must be edited (§5, item 2).
   
   ---
   
   ## 5. Impact — this is a breaking change
   
   Stated plainly, because it should not be understated:
   
   1. **Applications calling `fork()` on non-MMU targets stop linking.** This is
      the intended, correct outcome, and it is a build break for anyone doing it
      today. `CONFIG_FORK_IS_TASK_FORK` restores them exactly (§4.5).
   2. **Applications calling `fork()` on MMU targets change behaviour** — from
      sharing to copying. Code that (perhaps unknowingly) depended on the 
sharing
      will change behaviour. Such code was relying on non-POSIX behaviour, but 
it
      exists, and it will need `task_fork()` (§4.4) — or `vfork()` if it follows
      the fork-then-exec pattern.
   3. **In-tree users must be audited.** Any `apps/` code calling `fork()` 
needs to
      be checked for whether it means `fork()` or `vfork()`.
   4. **`ostest` needs work.** Its `vfork` test becomes a genuine `vfork()` test
      (and should use `_exit()` rather than `exit()`, per the POSIX contract — a
      `vfork()` child running `atexit` handlers and flushing stdio in the 
parent's
      address space is exactly the misuse the restriction exists to prevent). A
      separate `fork()` test should be added for configurations that provide it.
   5. **Per-architecture `select`s must be revisited**, since
      `CONFIG_ARCH_HAVE_FORK` changes meaning.
   
   Mitigations: the compatibility option covers legacy code with a one-line 
change;
   the migration path for the dominant fork+exec idiom is `posix_spawn()`, which
   NuttX already provides and already recommends (and which today's
   `fork()`+`exec()` cannot substitute for anyway, since the parent loses track 
of
   the child at exec — §1.5); and the change is staged — `vfork()` can be split
   out first, with `fork()` following per architecture as real support lands.
   
   On the default: if withdrawing `fork()` from non-MMU targets in a single
   release is judged too abrupt — it lands on ARM, the most-used architecture —
   `CONFIG_FORK_IS_TASK_FORK` can ship as `default y` with a deprecation warning
   for a release cycle, flipping to `default n` afterwards. The end state should
   still be `default n`: the honest behaviour has to become the one you get
   without asking. The schedule for reaching it is negotiable; the destination
   should not be.
   
   ---
   
   ## 6. What we gain
   
   * **`fork()` means one thing on every NuttX target.** Copy semantics, or it 
does
     not exist. No per-arch, per-build-model divergence.
   * **Portable applications become possible.** Code written against POSIX
     `fork()` works, or refuses to build. It never silently misbehaves.
   * **`vfork()` becomes honest and useful.** It means "share, cheaply", which 
is
     exactly what a no-MMU system can offer and exactly why a caller picks it —
     including on systems that *do* have an MMU.
   * **MMU-capable ports can implement real `fork()`.** This is already
     demonstrated working on ESP32-S3 `BUILD_KERNEL`; it just cannot be 
upstreamed
     coherently while the two functions are one.
   * **Windowed-ABI architectures get a workable story at all.** Xtensa cannot
     implement the current relocated-stack-copy hybrid — absolute stack pointers
     in the window save areas make it unwind onto the parent's stack (§4.2) —
     but it can implement both honest primitives: `vfork()` by borrowing,
     `fork()` by duplicating at the same virtual addresses.
   * **Nothing is lost.** Today's behaviour survives, exactly, as `task_fork()`
     (§4.4) — and via `CONFIG_FORK_IS_TASK_FORK` (§4.5) even the spelling
     `fork()` survives for legacy code on every configuration where it works
     today, until a configuration gains a real `fork()`, at which point its
     sharing-dependent callers move to the `task_fork()` spelling (§4.5, §5
     item 2).
   * **NuttX moves closer to POSIX**, not further, and closes a documentation 
defect
     in which `fork()` is specified using `vfork()`'s restrictions.
   
   ---
   
   ## 7. Implementation sketch
   
   1. Add `CONFIG_ARCH_HAVE_VFORK`; add `SYS_vfork` and `up_vfork()`.
   2. Move today's sharing implementation to `up_vfork()`, unchanged in
      behaviour: `nxtask_setup_fork()` keeps `addrenv_join()` on that path, and
      the libc `waitpid()` and the stack copy stay exactly as they are. The
      architecture's register/stack snapshot machinery is common to both
      primitives — the divergence is which address-environment operation
      `nxtask_setup_fork()` performs.
   3. `libs/libc/unistd/lib_fork.c`: `vfork()` calls `up_vfork()`; `fork()` 
calls
      `up_fork()`; `fork()` is compiled only under `CONFIG_ARCH_HAVE_FORK` (or
      aliased to `task_fork()` under `CONFIG_FORK_IS_TASK_FORK`).
   4. Rename today's per-arch `ARCH_HAVE_FORK` selects to `ARCH_HAVE_TASK_FORK`
      and expose the existing machinery as `task_fork()` (§4.4); add the
      compatibility alias `CONFIG_FORK_IS_TASK_FORK` (§4.5).
   5. Move the parent suspension out of libc into the kernel vfork path, so the
      parent resumes at `exec()` as well as `_exit()` (§1.5) and `vfork()` stops
      depending on `CONFIG_SCHED_WAITPID`. With the parent suspended, the child
      can borrow the parent's stack outright instead of copying it — required on
      windowed-ABI architectures such as Xtensa, an optimization elsewhere
      (§4.2).
   6. Redefine `CONFIG_ARCH_HAVE_FORK` to mean real `fork()`: it requires
      `CONFIG_ARCH_ADDRENV` plus a new `CONFIG_ARCH_HAVE_ADDRENV_FORK`, selected
      by architectures that can duplicate an address environment. Architectures
      gain the select only as real support lands — until then they have no
      `fork()`, which is accurate.
   7. Add the generic duplication path used by `fork()`: a new
      `addrenv_fork()` beside `addrenv_join()`, backed by an architecture hook
      (`up_addrenv_fork()`) that copies the parent's regions into freshly 
allocated
      pages mapped at the same virtual addresses.
   8. `ostest`: correct the `vfork` test to the POSIX contract (`_exit()`); add 
a
      `fork` test guarded by `CONFIG_ARCH_HAVE_FORK`; add a `task_fork` test
      guarded by `CONFIG_ARCH_HAVE_TASK_FORK` (today's `vfork` test, renamed, is
      very nearly that test already).
   
   Steps 1–4 are mechanical and independently reviewable — no behaviour changes
   for existing configurations. Step 5 is the first behaviour change, and it is
   confined to `vfork()`. Steps 6–8 can land per architecture.
   
   ---
   
   ## 8. References
   
   * POSIX.1-2017 `fork()` —
     <https://pubs.opengroup.org/onlinepubs/9699919799/functions/fork.html>
   * POSIX.1-2004 (SUSv3) `vfork()`, since withdrawn —
     <https://pubs.opengroup.org/onlinepubs/009695399/functions/vfork.html>
   * POSIX.1-2017 `_exit()` —
     <https://pubs.opengroup.org/onlinepubs/9699919799/functions/_exit.html>
   * POSIX.1-2017 `posix_spawn()` —
     
<https://pubs.opengroup.org/onlinepubs/9699919799/functions/posix_spawn.html>
   * Linux `vfork(2)` (standards notes: POSIX.1-2001 obsolescent, removed in
     POSIX.1-2008) — <https://man7.org/linux/man-pages/man2/vfork.2.html>
   * NuttX source referenced above, all at `apache/nuttx` master
     
[`216edd74d`](https://github.com/apache/nuttx/tree/216edd74db80d9b1db3c87683e14b03d8b34e279):
     
[`libs/libc/unistd/lib_fork.c`](https://github.com/apache/nuttx/blob/216edd74db80d9b1db3c87683e14b03d8b34e279/libs/libc/unistd/lib_fork.c),
     
[`sched/task/task_fork.c`](https://github.com/apache/nuttx/blob/216edd74db80d9b1db3c87683e14b03d8b34e279/sched/task/task_fork.c),
     
[`sched/task/task_execve.c`](https://github.com/apache/nuttx/blob/216edd74db80d9b1db3c87683e14b03d8b34e279/sched/task/task_execve.c),
     
[`sched/addrenv/addrenv.c`](https://github.com/apache/nuttx/blob/216edd74db80d9b1db3c87683e14b03d8b34e279/sched/addrenv/addrenv.c),
     
[`sched/pthread/pthread_create.c`](https://github.com/apache/nuttx/blob/216edd74db80d9b1db3c87683e14b03d8b34e279/sched/pthread/pthread_create.c),
     
[`include/sys/syscall_lookup.h`](https://github.com/apache/nuttx/blob/216edd74db80d9b1db3c87683e14b03d8b34e279/include/sys/syscall_lookup.h),
     
[`arch/Kconfig`](https://github.com/apache/nuttx/blob/216edd74db80d9b1db3c87683e14b03d8b34e279/arch/Kconfig),
     
[`arch/sim/src/sim/sim_fork.c`](https://github.com/apache/nuttx/blob/216edd74db80d9b1db3c87683e14b03d8b34e279/arch/sim/src/sim/sim_fork.c)
   * `apps` source, at `apache/nuttx-apps` master
     
[`6ca1272b9`](https://github.com/apache/nuttx-apps/tree/6ca1272b99494686731c53d22f81224d7ab8117d):
     
[`testing/ostest/vfork.c`](https://github.com/apache/nuttx-apps/blob/6ca1272b99494686731c53d22f81224d7ab8117d/testing/ostest/vfork.c)
   * History: nuttx
     
[`c33d1c9c97`](https://github.com/apache/nuttx/commit/c33d1c9c977680c5c1f5214c260269b782fdff09)
     *"sched/task/fork: add fork implementation"* (2023-07, the
     `vfork()`→`fork()` rename); nuttx
     
[`3524f4b9c`](https://github.com/apache/nuttx/commit/3524f4b9ce4e1c12dd30dda7cb9a161406485ee6)
     *"libs/libc/fork: add lib_fork implementation"* (2023-07, syscall renamed 
to
     `up_fork`); nuttx
     
[`2f2632338`](https://github.com/apache/nuttx/commit/2f263233884182726283676d972c2b6922619dd3)
     *"arch/libc: Integrate vfork into fork, and vfork directly call up_fork"*
     (2024-10); apps
     
[`79c7962af`](https://github.com/apache/nuttx-apps/commit/79c7962af67dc14a40bc140cb3377e2c8e7b99e0)
     *"apps: Replace CONFIG_ARCH_HAVE_VFORK with CONFIG_ARCH_HAVE_FORK"*
     (2023-08)
   
   
   ### Describe alternatives you've considered
   
   _No response_
   
   ### Verification
   
   - [x] I have verified before submitting the report.


-- 
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