Junbo-Zheng opened a new pull request, #20129:
URL: https://github.com/apache/nuttx/pull/20129
## Summary
`atexit_call_exitfuncs()` evaluated its loop bound once on entry (`for (idx
= aehead->nfuncs - 1; idx >= 0; idx--)`), while `atexit_register()` appends new
entries at `funcs[nfuncs]` and never decrements `nfuncs`. Any function
registered by an exit handler via `atexit()` / `on_exit()` / `__cxa_atexit()`
therefore lands above the cached bound and is never invoked -- even though the
registration returns OK. This is the standard C++ code path where a
function-local static's destructor is registered by `__cxa_atexit` at
construction time, and that construction first runs during exit processing.
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 fix restructures the consumption loop into `while (aehead->nfuncs > 0)`:
the newest entry is claimed under
`ta_lock`, cleared, and `nfuncs--` frees the slot (a registration made
during exit lands exactly there and runs on the next iteration, before the
older remaining entries -- the documented semantics); the handler is invoked
with the lock released.
The same restructure closes a second defect: `atexit_call_exitfuncs()`
previously read and cleared the task-group-shared `ta_exit` list without
holding `ta_lock`, while `atexit_register()` takes it ("The following must be
atomic"). Claiming entries under the lock closes that data race; calling
handlers with the lock released keeps a handler re-entering `atexit_register()`
deadlock-free on the non-recursive `nxmutex`.
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 --
https://github.com/apache/nuttx/blob/5a209a853ec0dac623a2d5dfa81dea546b22820d/libs/libc/stdlib/lib_exit.c#L65-L70
## Impact
- **Users**: functions registered during exit processing (from an atexit
handler, or via `__cxa_atexit` from a C++ destructor chain) are now actually
invoked, in the documented order (newest first, ahead of older remaining
handlers). Previously they were silently dropped while `atexit()` reported
success.
- **Build**: None.
- **Hardware**: None -- pure libc-level fix, no board specifics.
- **Documentation**: None -- the code now matches the behavior already
documented in the exit() docstring and the exit(3) man page.
- **Security & Compatibility**: closes a data race on the task-group shared
exit-function list (consume side now holds `ta_lock`). No API change.
## Testing
Simulated (sim:nsh host build on Ubuntu x86-64, `CONFIG_LIBC_MAX_EXITFUNS=8`
-- the sim default is 1).
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
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 PR); its diff:
```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
```
Signed-off-by: Junbo Zheng <[email protected]>
--
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]