From 80dd244548d8932bd558f576583318019d0e725a Mon Sep 17 00:00:00 2001 From: Junbo Zheng Date: Sun, 13 Sep 2026 18:07:43 +0800 Subject: [PATCH] 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 #include +#include /**************************************************************************** * 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) Signed-off-by: Junbo Zheng --- 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 5a3322c412c1c..cbc1e072b7faf 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) {