This is an automated email from the ASF dual-hosted git repository.
acassis pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/nuttx.git
The following commit(s) were added to refs/heads/master by this push:
new 7797b122443 libc/atexit: honor registrations made during exit
processing
7797b122443 is described below
commit 7797b1224433cd2324c1745fce91e77d1ad3696a
Author: Junbo Zheng <[email protected]>
AuthorDate: Sun Sep 13 18:07:43 2026 +0800
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 --
https://github.com/apache/nuttx/blob/5a209a853ec0dac623a2d5dfa81dea546b22820d/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) <[email protected]>
Signed-off-by: Junbo Zheng <[email protected]>
---
libs/libc/stdlib/lib_atexit.c | 22 +++++++++++++++++++---
1 file changed, 19 insertions(+), 3 deletions(-)
diff --git a/libs/libc/stdlib/lib_atexit.c b/libs/libc/stdlib/lib_atexit.c
index 5a3322c412c..cbc1e072b7f 100644
--- a/libs/libc/stdlib/lib_atexit.c
+++ b/libs/libc/stdlib/lib_atexit.c
@@ -110,6 +110,7 @@ int atexit_register(int type, CODE void (*func)(void), FAR
void *arg,
void atexit_call_exitfuncs(int status, bool quick)
{
FAR struct atexit_list_s *aehead;
+ FAR struct task_info_s *info = task_get_info();
CODE void (*func)(void);
FAR void *arg;
int idx;
@@ -119,17 +120,32 @@ void atexit_call_exitfuncs(int status, bool quick)
aehead = get_exitfuncs();
- for (idx = aehead->nfuncs - 1; idx >= 0; idx--)
+ while (aehead->nfuncs > 0)
{
- /* Remove the function to prevent recursive call to it */
+ /* Claim the newest entry under the lock. A handler may register
+ * further functions during exit processing; those land in the slot
+ * just freed here and are executed on the next iteration, i.e.
+ * before the older remaining ones, as documented in exit(3).
+ */
- type = aehead->funcs[idx].type;
+ if (nxmutex_lock(&info->ta_lock) < 0)
+ {
+ break;
+ }
+
+ idx = aehead->nfuncs - 1;
+ type = aehead->funcs[idx].type;
func = aehead->funcs[idx].func;
arg = aehead->funcs[idx].arg;
+ /* Remove the function to prevent recursive call to it */
+
aehead->funcs[idx].func = NULL;
aehead->funcs[idx].arg = NULL;
+ aehead->nfuncs--;
+
+ nxmutex_unlock(&info->ta_lock);
if (!func)
{