nuttx/libs/libc/libc.csv
Marco Casaroli a229e8b87e sched/arch/libc: give fork(), vfork() and task_fork() separate semantics
NuttX implemented fork() and vfork() as the same function.  Both were libc
wrappers around a single up_fork() syscall; vfork() differed only by a
trailing waitpid().  Underneath, the child joined the parent's address
environment -- the same addrenv_join() that pthread_create() uses -- and got
a private copy of the stack.  So the child shared .data, .bss and the heap
with its parent and ran concurrently with it.

That is not fork().  It is vfork()-with-a-private-stack under fork()'s name,
and the history says so: today's fork() is NuttX's old vfork(), renamed in
c33d1c9c97 (2023) without any change of behaviour.  The failure was silent --
a program written against POSIX fork() compiled, ran, and had its child's
writes land in the parent's variables.

Separate them into three primitives, chosen by which function the caller
called rather than by what the hardware happens to be:

  fork()       child gets its own copy of the parent's memory at the same
               virtual addresses; runs concurrently.  Only where an address
               environment can be duplicated -- elsewhere it is not declared
               at all, so calling it is a build error naming the function.
  vfork()      child shares the parent's memory; parent suspended until the
               child _exit()s or exec()s.  Implementable everywhere.
  task_fork()  the historical behaviour under an honest name: shares memory,
               private stack copy, both running.  Non-POSIX, in sched.h.

Below libc there are now three syscalls -- up_task_fork(), up_vfork() and
up_fork().  The per-arch register snapshot is common to all three; each
architecture's entry points share one sequence and differ only in a
FORK_TYPE_* selector (include/nuttx/fork.h) handed to nxtask_setup_fork(),
which is the single place the memory semantics are decided.

The vfork() parent suspension moves out of libc into nxtask_start_vfork(),
released from nxsched_release_tcb().  Two things follow: the parent is
resumed at exec(), since exec_swap() has already handed the child's pid to
the loaded program by the time the vfork stub exits, and vfork() no longer
depends on CONFIG_SCHED_WAITPID.

Releasing there requires one fix in nxtask_exit().  It raises rtcb->lockcount
directly rather than through sched_lock() while it tears the TCB down, so the
nxsem_post() that wakes the vfork() parent queues it on g_pendingtasks -- and
the matching raw lockcount-- does not merge that list the way sched_unlock()
would, leaving the parent stranded with nothing left to move it off.  A
nxsched_merge_pending() after the decrement publishes it.  The call is a
no-op while pre-emption is still disabled, and up_exit() re-reads this_task()
afterwards, so a change of the ready-to-run head is honoured.  Without it
vfork() deadlocks on any configuration where no other task happens to call
sched_unlock() afterwards -- rv-virt:nsh64 and rv-virt:pnsh64, for instance,
where NSH is blocked in waitpid() holding the lock.

fork() is built on a new addrenv_fork(), backed by an up_addrenv_fork() hook
that duplicates an address environment into freshly allocated pages mapped at
the same virtual addresses -- unlike up_addrenv_clone(), which copies only
the representation and leaves both pointing at the same page tables.  The
child then adopts the parent's stack geometry rather than being given a
relocated copy: a pointer to a stack local taken before fork() must name the
same object in the child that it named in the parent, and the parent's stack
is already in the duplicate, with its contents, at the parent's address.

No architecture implements up_addrenv_fork() yet, so this commit leaves
fork() unavailable everywhere.  That is the intended state.  It withdraws
fork() from ARCH_ARM, flat ARCH_ARM64, ARCH_RISCV, ARCH_SIM and ARCH_X86_64,
where until now it named the sharing primitive; per-architecture patches
restore it, with POSIX semantics, as up_addrenv_fork() lands.  Nothing is
lost in the meantime: task_fork() is that same sharing primitive under its
own name, and CONFIG_FORK_IS_TASK_FORK (default n) aliases fork() back to it
for legacy code, on exactly the configurations that had fork() before.

Kconfig: ARCH_HAVE_TASK_FORK and ARCH_HAVE_VFORK inherit ARCH_HAVE_FORK's
select lines, conditions included, so no configuration gains machinery;
ARCH_HAVE_FORK is redefined to mean "can provide POSIX fork() semantics" and
derives from the new ARCH_HAVE_ADDRENV_FORK.

There is one deliberate departure from "verbatim".  ARCH_ARM selected the
fork family unconditionally, BUILD_KERNEL included, and that has never
worked:  on a kernel build the architecture's fork entry point sees the
kernel's return address and stack pointer rather than the caller's, so the
child resumes at a kernel address.  On qemu-armv7a:knsh master faults in
ostest's task_fork case with "Child did not run" and then a data abort;
without the condition this change faults the same way through vfork().
ARCH_ARM64 and ARCH_X86_64 already carried "if !BUILD_KERNEL" for exactly
this reason -- ARM was the outlier.  Conditioning it turns a runtime fault
into an honest absence, which is the whole point of the change; arch/arm
takes the condition off again in the patch that adds its saved-syscall-frame
path.  Only the MMU-capable ARM ports are affected, since Cortex-M cannot
build BUILD_KERNEL at all.

Also fixes two latent syntax errors found on the way: a missing comma in
riscv_fork.c and mips_fork.c, both in *_FRAMEPOINTER && !SAVE_GP branches
that are never compiled today.

Signed-off-by: Marco Casaroli <marco.casaroli@gmail.com>

Assisted-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 16:39:07 +02:00

30 KiB

1__assertassert.hvoidFAR const char *intFAR const char *
2__cxa_atexitstdlib.hintFAR CODE void (*)(FAR void *)|FAR void *FAR void *FAR void *
3aio_suspendaio.hdefined(CONFIG_FS_AIO)intFAR const struct aiocb * const []|FAR const struct aiocb * const *intFAR const struct timespec *
4asprintfstdio.hintFAR char **FAR const IPTR char *...
5crc32partnuttx/crc32.huint32_tFAR const uint8_t *size_tuint32_t
6dq_addafternuttx/queue.hvoidFAR dq_entry_t *FAR dq_entry_t *FAR dq_queue_t *
7fgetsstdio.hdefined(CONFIG_FILE_STREAM)FAR char *FAR char *intFAR FILE *
8fnmatchfnmatch.hintFAR const char *FAR const char *int
9fprintfstdio.hdefined(CONFIG_FILE_STREAM)intFAR FILE *FAR const IPTR char *...
10fscanfstdio.hintFAR FILE *FAR const char *...
11fseekstdio.hdefined(CONFIG_FILE_STREAM)intFAR FILE *long intint
12getoptunistd.hintintFAR char * const []|FAR char * const *FAR const char *
13getrandomsys/random.h!defined(CONFIG_BUILD_KERNEL)ssize_tFAR void *size_tunsigned int
14inet_ptonarpa/inet.hintintFAR const char *FAR void *
15lib_dumpbufferdebug.hvoidFAR const char *FAR const uint8_t *unsigned int
16mbrlenwchar.hsize_tFAR const char *size_tFAR mbstate_t *
17mbstowcsstdlib.hsize_tFAR wchar_t *FAR const char *size_t
18mbtowcstdlib.hintFAR wchar_t *FAR const char *size_t
19memchrstring.hFAR void *FAR const void *intsize_t
20memcmpstring.hintFAR const void *FAR const void *size_t
21memcpystring.hFAR void *FAR void *FAR const void *size_t
22memmovestring.hFAR void *FAR void *FAR const void *size_t
23memsetstring.hFAR void *FAR void *intsize_t
24posix_fallocatefcntl.hintintoff_toff_t
25posix_memalignstdlib.hintFAR void **size_tsize_t
26pthread_barrier_initpthread.h!defined(CONFIG_DISABLE_PTHREAD)intFAR pthread_barrier_t *FAR const pthread_barrierattr_t *unsigned int
27pthread_cond_timedwaitpthread.h!defined(CONFIG_DISABLE_PTHREAD)intFAR pthread_cond_t *FAR pthread_mutex_t *FAR const struct timespec *
28pthread_getname_nppthread.h!defined(CONFIG_DISABLE_PTHREAD)intpthread_tchar *size_t
29pthread_mutex_setprioceilingpthread.h!defined(CONFIG_DISABLE_PTHREAD) && defined(CONFIG_PRIORITY_PROTECT)intFAR pthread_mutex_t *intFAR int *
30readdir_rdirent.hintFAR DIR *FAR struct dirent *FAR struct dirent **
31readvsys/uio.hssize_tintFAR const struct iovec *int
32sem_initsemaphore.hintFAR sem_t *intunsigned int
33setprioritysys/resource.hintintid_tint
34sprintfstdio.hintFAR char *FAR const IPTR char *...
35sprintfstdio.hintFAR char *FAR const IPTR char *...
36sq_addafternuttx/queue.hvoidFAR sq_entry_t *FAR sq_entry_t *FAR sq_queue_t *
37sscanfstdio.hintFAR const IPTR char *FAR const char *...
38sscanfstdio.hintFAR const char *FAR const char *...
39strerror_rstring.hintintFAR char *size_t
40strlcpystring.hsize_tFAR char *FAR const char *size_t
41strncasecmpstrings.hintFAR const char *FAR const char *size_t
42strncatstring.hFAR char *FAR char *FAR const char *size_t
43strncmpstring.hintFAR const char *FAR const char *size_t
44strncpystring.hFAR char *FAR char *FAR const char *size_t
45strtoimaxinttypes.hintmax_tFAR const char *FAR char **int
46strtok_rstring.hFAR char *FAR char *FAR const char *FAR char **
47strtolstdlib.hlongFAR const char *FAR char **int
48strtollstdlib.hlong longFAR const char *FAR char **int
49strtoulstdlib.hunsigned longFAR const char *FAR char **int
50strtoullstdlib.hunsigned long long intFAR const char *FAR char **int
51strtoullstdlib.hunsigned long longFAR const char *FAR char **int
52strtoumaxinttypes.huintmax_tFAR const char *FAR char **int
53strxfrmstring.hdefined(CONFIG_LIBC_LOCALE)size_tFAR char *FAR const char *size_t
54swabunistd.hvoidFAR const void *FAR void *ssize_t
55syslogsyslog.hvoidintFAR const IPTR char *...
56tcsetattrtermios.hintintintFAR const struct termios *
57vasprintfstdio.hintFAR char **FAR const IPTR char *va_list
58vfprintfstdio.hdefined(CONFIG_FILE_STREAM)intFAR FILE *FAR const IPTR char *va_list
59vsprintfstdio.hintFAR char *FAR const IPTR char *va_list
60vsscanfstdio.hintFAR const char *FAR const IPTR char *va_list
61vsyslogsyslog.hvoidintFAR const IPTR char *va_list
62wcrtombwchar.hsize_tFAR char *wchar_tFAR mbstate_t *
63wcslcatwchar.hsize_tFAR wchar_t *FAR const wchar_t *size_t
64wcsncatwchar.hFAR wchar_t *FAR wchar_t *FAR const wchar_t *size_t
65wcsncmpwchar.hintFAR const wchar_t *FAR const wchar_t *size_t
66wcsncpywchar.hFAR wchar_t *FAR wchar_t *FAR const wchar_t *size_t
67wcslcpywchar.hsize_tFAR wchar_t *FAR const wchar_t *size_t
68wcstokwchar.hFAR wchar_t *FAR wchar_t *FAR const wchar_t *FAR wchar_t **
69wcstolwchar.hlong intFAR const wchar_t *FAR wchar_t **int
70wcstollwchar.hlong long intFAR const wchar_t *FAR wchar_t **int
71wcstombsstdlib.hsize_tFAR char *FAR const wchar_t *size_t
72wcstoulwchar.hunsigned long intFAR const wchar_t *FAR wchar_t **int
73wcsxfrmwchar.hsize_tFAR wchar_t *FAR const wchar_t *size_t
74wmemchrwchar.hFAR wchar_t *FAR const wchar_t *wchar_tsize_t
75wmemcmpwchar.hintFAR const wchar_t *FAR const wchar_t *size_t
76wmemcpywchar.hFAR wchat_t *FAR wchar_t *FAR const wchar_t *size_t
77wmemmovewchar.hFAR wchat_t *FAR wchar_t *FAR const wchar_t *size_t
78wmemsetwchar.hFAR wchat_t *FAR wchar_t *wchar_tsize_t
79writevsys/uio.hssize_tintFAR const struct iovec *int