nuttx/libs
Junbo Zheng 7797b12244
Some checks are pending
Build Documentation / build-html (push) Waiting to run
MemBrowse Memory Report / changes-filter (push) Waiting to run
MemBrowse Memory Report / load-targets (push) Waiting to run
MemBrowse Memory Report / identical (push) Blocked by required conditions
MemBrowse Memory Report / analyze (push) Blocked by required conditions
libc/atexit: honor registrations made during exit processing
atexit_call_exitfuncs() cached its loop bound on entry
(for (idx = aehead->nfuncs - 1; idx >= 0; idx--)), while
atexit_register() appends new entries at funcs[nfuncs] and bumps nfuncs.
Any function registered by an exit handler via atexit() / on_exit() /
__cxa_atexit() lands above the cached bound and is never invoked, even
though the registration returns OK.

This contradicts the exit(3) documentation that NuttX mirrors verbatim
in its own exit() docstring (libs/libc/stdlib/lib_exit.c):

  It is possible for one of these functions to use atexit(3) or
  on_exit(3) to register an additional function to be executed
  during exit processing; the new registration is added to the
  front of the list of functions that remain to be called.

The same restructure closes a second defect: atexit_call_exitfuncs()
read and cleared the task-group-shared ta_exit list without holding
ta_lock, while atexit_register() takes it ("The following must be
atomic").  Entries are now claimed under the lock and the handler is
invoked with the lock released, so a handler re-entering
atexit_register() cannot deadlock (also safe with the non-recursive
nxmutex used here).

Evidence: exit(3) man page, DESCRIPTION -
https://man7.org/linux/man-pages/man3/exit.3.html
NuttX mirrors this passage verbatim in its own exit() docstring --
5a209a853e/libs/libc/stdlib/lib_exit.c (L65-L70)

Before:
```
A handler that registers another function during exit processing
gets a success return from atexit(), but the new function is never
invoked - it lands above the loop bound cached on entry.
```

After:
```
A registration made during exit processing runs before the older
remaining handlers (order A -> B -> C below), matching the exit(3)
guarantee, and the list is consumed under ta_lock.
```

Testing:

Simulated (sim:nsh, CONFIG_LIBC_MAX_EXITFUNS=8).

Build and run:
```
cmake -B build -DBOARD_CONFIG=sim:nsh -GNinja
cmake -S . -B build   # after setting CONFIG_LIBC_MAX_EXITFUNS=8
                       # in build/.config (sim:nsh default is 1)
cmake --build build -j$(nproc)
(echo hello; echo poweroff) | ./build/nuttx
```
"hello" runs the test at the NSH prompt; poweroff terminates the sim.

The test was carried by apps/examples/hello/hello_main.c (scratch only,
not part of this commit); its diff:

```
--- a/examples/hello/hello_main.c
+++ b/examples/hello/hello_main.c
@@ -24,6 +24,7 @@

 #include <nuttx/config.h>
 #include <stdio.h>
+#include <stdlib.h>

 /****************************************************************************
  * Public Functions
@@ -33,8 +34,29 @@
  * hello_main
  ****************************************************************************/

+static void handler_b(void)
+{
+  printf("ATEXIT-TEST: handler B called (registered during exit)\n");
+}
+
+static void handler_a(void)
+{
+  int ret;
+
+  printf("ATEXIT-TEST: handler A called\n");
+  ret = atexit(handler_b);
+  printf("ATEXIT-TEST: atexit(handler_b) inside A returned %d\n", ret);
+}
+
+static void handler_c(void)
+{
+  printf("ATEXIT-TEST: handler C called\n");
+}
+
 int main(int argc, FAR char *argv[])
 {
   printf("Hello, World!!\n");
+  atexit(handler_c);   /* older entry, must run LAST */
+  atexit(handler_a);   /* registers handler_b during exit */
   return 0;
 }
```

Before the fix:
```
Hello, World!!
ATEXIT-TEST: handler A called
ATEXIT-TEST: atexit(handler_b) inside A returned 0
ATEXIT-TEST: handler C called
```
(handler B is never invoked although its registration returned 0)

After the fix:
```
Hello, World!!
ATEXIT-TEST: handler A called
ATEXIT-TEST: atexit(handler_b) inside A returned 0
ATEXIT-TEST: handler B called (registered during exit)
ATEXIT-TEST: handler C called
```

Assisted-by: Claude Code (GLM-5.3) <claude@anthropic.com>
Signed-off-by: Junbo Zheng <zhengjunbo1@xiaomi.com>
2026-09-14 18:52:46 -03:00
..
libbuiltin !sched/arch/libc: Give fork() and vfork() their real, separate semantics. 2026-08-10 08:57:30 -03:00
libc libc/atexit: honor registrations made during exit processing 2026-09-14 18:52:46 -03:00
libdsp tools: fix stale archive members surviving a Kconfig-driven CSRCS change 2026-07-28 21:26:03 -03:00
libm cmake: Use NUTTX(_DIR/_BIN_DIR) instead CMAKE(_SRC_DIR/_BIN_DIR) 2026-08-09 11:13:08 -03:00
libnx tools: fix stale archive members surviving a Kconfig-driven CSRCS change 2026-07-28 21:26:03 -03:00
libxx cmake: Use NUTTX(_DIR/_BIN_DIR) instead CMAKE(_SRC_DIR/_BIN_DIR) 2026-08-09 11:13:08 -03:00
CMakeLists.txt libs: migrate to SPDX identifier 2024-10-01 12:25:52 +08:00