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]
