From: Arran Cudbard-Bell Date: Tue, 28 Jun 2022 18:40:45 +0000 (-0500) Subject: Rework the atexit code to function without pthreads X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=a7e5dfbc0528303bdebd9af2dad5b12143469cd3;p=thirdparty%2Ffreeradius-server.git Rework the atexit code to function without pthreads --- diff --git a/src/include/build.h b/src/include/build.h index 0b96f19bf00..d139d5d56e3 100644 --- a/src/include/build.h +++ b/src/include/build.h @@ -65,6 +65,18 @@ extern "C" { #include #include +/* + * These are compile time options to toggle whether + * we're building with thread support. + * + * With EMSCRIPTEN threading support isn't guaranteed + * as many browsers have explicitly disabled support + * due to spectre attacks. + */ +#if (defined(__EMSCRIPTEN__) && defined(__EMSCRIPTEN_PTHREADS__)) || !defined(__EMSCRIPTEN__) && defined(HAVE_PTHREADS_H) +# define HAVE_PTHREADS 1 +#endif + /* * GCC will sometimes define "unix" as well as "__unix", * which gets confusing and is unnecessary. diff --git a/src/lib/util/atexit.c b/src/lib/util/atexit.c index f7745dd4474..2e032bb174f 100644 --- a/src/lib/util/atexit.c +++ b/src/lib/util/atexit.c @@ -30,8 +30,9 @@ RCSID("$Id$") #include #include - +#ifdef HAVE_PTHREADS #include +#endif #if defined(DEBUG_ATEXIT) && !defined(NDEBUG) # define ATEXIT_DEBUG FR_FAULT_LOG @@ -68,12 +69,14 @@ struct fr_exit_handler_list_s { ///< to ensure this memory is cleaned up. }; +#ifdef HAVE_PTHREADS static _Thread_local fr_atexit_list_t *fr_atexit_thread_local = NULL; static fr_atexit_list_t *fr_atexit_threads = NULL; -static fr_atexit_list_t *fr_atexit_global = NULL; static pthread_mutex_t fr_atexit_global_mutex = PTHREAD_MUTEX_INITIALIZER; -static bool is_exiting; +#endif +static fr_atexit_list_t *fr_atexit_global = NULL; +static bool is_exiting; /** Call the exit handler * @@ -125,36 +128,6 @@ static fr_atexit_entry_t *atexit_entry_alloc(NDEBUG_LOCATION_ARGS return e; } -/** Talloc destructor for freeing list elements in order - * - */ -static int _thread_local_list_free(fr_atexit_list_t *list) -{ - ATEXIT_DEBUG("%s - Freeing _Thread_local destructor list %p", __FUNCTION__, list); - - fr_dlist_talloc_free(&list->head); /* Free in order */ - list->e->func = NULL; /* Disarm the global entry that'd free the thread-specific list */ - return 0; -} - -/** Run all the thread local destructors - * - * @param[in] list The thread-specific exit handler list. - */ -static void _thread_local_pthread_free(void *list) -{ - talloc_free(list); -} - -/** Run all the thread local destructors - * - * @param[in] list The thread-specific exit handler list. - */ -static int _thread_local_free(void *list) -{ - return talloc_free(list); -} - /** Talloc destructor for freeing list elements in order * */ @@ -171,12 +144,17 @@ static int _destructor_list_free(fr_atexit_list_t *list) */ static void _global_free(void) { +#ifdef HAVE_PTHREADS pthread_mutex_lock(&fr_atexit_global_mutex); +#endif + fr_cond_assert_msg(!is_exiting, "Global free function called multiple times"); is_exiting = true; - pthread_mutex_unlock(&fr_atexit_global_mutex); +#ifdef HAVE_PTHREADS + pthread_mutex_unlock(&fr_atexit_global_mutex); TALLOC_FREE(fr_atexit_threads); /* Forcefully cleanup any thread-specific memory */ +#endif TALLOC_FREE(fr_atexit_global); } @@ -195,6 +173,7 @@ int fr_atexit_global_setup(void) fr_dlist_talloc_init(&fr_atexit_global->head, fr_atexit_entry_t, entry); talloc_set_destructor(fr_atexit_global, _destructor_list_free); +#ifdef HAVE_PTHREADS fr_atexit_threads = talloc_zero(NULL, fr_atexit_list_t); if (unlikely(!fr_atexit_threads)) return -1; @@ -202,12 +181,14 @@ int fr_atexit_global_setup(void) fr_dlist_talloc_init(&fr_atexit_threads->head, fr_atexit_entry_t, entry); talloc_set_destructor(fr_atexit_threads, _destructor_list_free); +#endif atexit(_global_free); /* Call all remaining destructors at process exit */ return 0; } +#ifdef HAVE_PTHREADS #define CHECK_GLOBAL_SETUP() \ do { \ int _ret = 0; \ @@ -217,6 +198,15 @@ do { \ pthread_mutex_unlock(&fr_atexit_global_mutex); \ if (_ret < 0) return _ret; \ } while(0) +#else +#define CHECK_GLOBAL_SETUP() \ +do { \ + int _ret = 0; \ + fr_cond_assert_msg(!is_exiting, "New atexit handlers should not be allocated whilst exiting"); \ + if (!fr_atexit_global) _ret = fr_atexit_global_setup(); \ + if (_ret < 0) return _ret; \ +} while(0) +#endif /** Add a free function to be called when the process exits * @@ -231,190 +221,6 @@ int _atexit_global(NDEBUG_LOCATION_ARGS return 0; } -/** Add a new destructor - * - * @return - * - 0 on success. - * - -1 on memory allocation failure; - */ -int _fr_atexit_thread_local(NDEBUG_LOCATION_ARGS - fr_atexit_t func, void const *uctx) -{ - CHECK_GLOBAL_SETUP(); - - /* - * Initialise the thread local list, just for pthread_exit(). - */ - if (!fr_atexit_thread_local) { - fr_atexit_list_t *list; - - /* - * Must be heap allocated, because thread local - * structures can be freed before the key - * destructor is run (depending on platform). - */ - list = talloc_zero(NULL, fr_atexit_list_t); - if (unlikely(!list)) return -1; - - ATEXIT_DEBUG("%s - Thread %u alloced _Thread_local destructor list %p", - __FUNCTION__, - (unsigned int)pthread_self(), list); - - fr_dlist_talloc_init(&list->head, fr_atexit_entry_t, entry); - (void) pthread_key_create(&list->key, _thread_local_pthread_free); - - /* - * We need to pass in a pointer to the heap - * memory because, again, the thread local - * indirection table may have disappeared - * by the time the thread destructor is - * called. - */ - (void) pthread_setspecific(list->key, list); - talloc_set_destructor(list, _thread_local_list_free); - - /* - * Add a destructor for the thread-local list - * The pthread based destructor will disarm - * this if it fires, but leave it enabled if - * it doesn't, thus ensuring the memory is - * *always* freed one way or another. - */ - pthread_mutex_lock(&fr_atexit_global_mutex); - list->e = atexit_entry_alloc(NDEBUG_LOCATION_VALS - fr_atexit_threads, - _thread_local_free, - list); - - pthread_mutex_unlock(&fr_atexit_global_mutex); - - fr_atexit_thread_local = list; - } - - /* - * Now allocate the actual atexit handler entry - */ - if (atexit_entry_alloc(NDEBUG_LOCATION_VALS fr_atexit_thread_local, func, uctx) == NULL) return -1; - - return 0; -} - -/** Remove a specific destructor for this thread (without executing them) - * - * @note This function's primary purpose is to help diagnose issues with destructors - * from within a debugger. - * - * @param[in] uctx_scope Only process entries where the func and scope both match. - * @param[in] func Entries matching this function will be disarmed. - * @param[in] uctx associated with the entry. - * @return How many destructors were disarmed. - */ -unsigned int fr_atexit_thread_local_disarm(bool uctx_scope, fr_atexit_t func, void const *uctx) -{ - fr_atexit_entry_t *e = NULL; - unsigned int count = 0; - - if (!fr_atexit_thread_local) return -1; - - while ((e = fr_dlist_next(&fr_atexit_thread_local->head, e))) { - fr_atexit_entry_t *disarm; - - if ((e->func != func) || ((e->uctx != uctx) && uctx_scope)) continue; - - ATEXIT_DEBUG("%s - Thread %u disarming %p/%p func=%p, uctx=%p (alloced %s:%u)", - __FUNCTION__, - (unsigned int)pthread_self(), - fr_atexit_thread_local, e, e->func, e->uctx, e->file, e->line); - disarm = e; - e = fr_dlist_remove(&fr_atexit_thread_local->head, e); - talloc_set_destructor(disarm, NULL); - talloc_free(disarm); - - count++; - } - - return count; -} - -/** Remove all destructors for this thread (without executing them) - * - * @note This function's primary purpose is to help diagnose issues with destructors - * from within a debugger. - */ -void fr_atexit_thread_local_disarm_all(void) -{ - fr_atexit_entry_t *e = NULL; - - if (!fr_atexit_thread_local) return; - - while ((e = fr_dlist_pop_head(&fr_atexit_thread_local->head))) { - ATEXIT_DEBUG("%s - Thread %u disarming %p/%p func=%p, uctx=%p (alloced %s:%u)", - __FUNCTION__, - (unsigned int)pthread_self(), - fr_atexit_thread_local, e, e->func, e->uctx, e->file, e->line); - talloc_set_destructor(e, NULL); - talloc_free(e); - } -} - -/** Cause all thread local free triggers to fire - * - * This is necessary when we're running in single threaded mode - * to ensure all "thread-local" memory (which isn't actually thread local) - * is cleaned up. - * - * One example is the OpenSSL log BIOs which must be cleaned up - * before fr_openssl_free is called. - * - * @return - * - >= 0 The number of atexit handlers triggered on success. - * - <0 the return code from any atexit handlers that returned an error. - */ -int fr_atexit_thread_trigger_all(void) -{ - fr_atexit_entry_t *e = NULL, *ee, *to_free; - fr_atexit_list_t *list; - unsigned int count = 0; - - /* - * Iterate over the list of thread local - * destructor lists running the - * destructors. - */ - while ((e = fr_dlist_next(&fr_atexit_threads->head, e))) { - if (!e->func) continue; /* thread already joined */ - - list = talloc_get_type_abort(e->uctx, fr_atexit_list_t); - ee = NULL; - while ((ee = fr_dlist_next(&list->head, ee))) { - ATEXIT_DEBUG("%s - Thread %u triggering %p/%p func=%p, uctx=%p (alloced %s:%u)", - __FUNCTION__, - (unsigned int)pthread_self(), - list, ee, ee->func, ee->uctx, ee->file, ee->line); - - count++; - to_free = ee; - ee = fr_dlist_remove(&list->head, ee); - if (talloc_free(to_free) < 0) { - fr_strerror_printf_push("atexit handler failed %p/%p func=%p, uctx=%p" -#ifndef NDEBUG - " (alloced %s:%u)" -#endif - , - list, to_free, - to_free->func, to_free->uctx -#ifndef NDEBUG - , to_free->file, to_free->line -#endif - ); - return -1; - } - } - } - - return count; -} - /** Remove a specific global destructor (without executing it) * * @note This function's primary purpose is to help diagnose issues with destructors @@ -430,8 +236,6 @@ unsigned int fr_atexit_global_disarm(bool uctx_scope, fr_atexit_t func, void con fr_atexit_entry_t *e = NULL; unsigned int count = 0; - if (!fr_atexit_thread_local) return -1; - while ((e = fr_dlist_next(&fr_atexit_global->head, e))) { fr_atexit_entry_t *disarm; @@ -537,8 +341,11 @@ int fr_atexit_global_trigger_all(void) */ int fr_atexit_trigger(bool uctx_scope, fr_atexit_t func, void const *uctx) { - fr_atexit_entry_t *e = NULL, *ee, *to_free; + fr_atexit_entry_t *e = NULL, *to_free; +#ifdef HAVE_PTHREADS + fr_atexit_entry_t *ee; fr_atexit_list_t *list; +#endif unsigned int count = 0; if (!fr_atexit_global) goto do_threads; @@ -574,6 +381,7 @@ int fr_atexit_trigger(bool uctx_scope, fr_atexit_t func, void const *uctx) e = NULL; do_threads: +#ifdef HAVE_PTHREADS if (!fr_atexit_threads) return 0; /* @@ -613,6 +421,7 @@ do_threads: } } } +#endif return count; } @@ -627,3 +436,219 @@ bool fr_atexit_is_exiting(void) { return is_exiting; } + +#ifdef HAVE_PTHREADS +/** Talloc destructor for freeing list elements in order + * + */ +static int _thread_local_list_free(fr_atexit_list_t *list) +{ + ATEXIT_DEBUG("%s - Freeing _Thread_local destructor list %p", __FUNCTION__, list); + + fr_dlist_talloc_free(&list->head); /* Free in order */ + list->e->func = NULL; /* Disarm the global entry that'd free the thread-specific list */ + return 0; +} + +/** Run all the thread local destructors + * + * @param[in] list The thread-specific exit handler list. + */ +static void _thread_local_pthread_free(void *list) +{ + talloc_free(list); +} + +/** Run all the thread local destructors + * + * @param[in] list The thread-specific exit handler list. + */ +static int _thread_local_free(void *list) +{ + return talloc_free(list); +} + +/** Add a new destructor + * + * @return + * - 0 on success. + * - -1 on memory allocation failure; + */ +int _fr_atexit_thread_local(NDEBUG_LOCATION_ARGS + fr_atexit_t func, void const *uctx) +{ + CHECK_GLOBAL_SETUP(); + + /* + * Initialise the thread local list, just for pthread_exit(). + */ + if (!fr_atexit_thread_local) { + fr_atexit_list_t *list; + + /* + * Must be heap allocated, because thread local + * structures can be freed before the key + * destructor is run (depending on platform). + */ + list = talloc_zero(NULL, fr_atexit_list_t); + if (unlikely(!list)) return -1; + + ATEXIT_DEBUG("%s - Thread %u alloced _Thread_local destructor list %p", + __FUNCTION__, + (unsigned int)pthread_self(), list); + + fr_dlist_talloc_init(&list->head, fr_atexit_entry_t, entry); + (void) pthread_key_create(&list->key, _thread_local_pthread_free); + + /* + * We need to pass in a pointer to the heap + * memory because, again, the thread local + * indirection table may have disappeared + * by the time the thread destructor is + * called. + */ + (void) pthread_setspecific(list->key, list); + talloc_set_destructor(list, _thread_local_list_free); + + /* + * Add a destructor for the thread-local list + * The pthread based destructor will disarm + * this if it fires, but leave it enabled if + * it doesn't, thus ensuring the memory is + * *always* freed one way or another. + */ + pthread_mutex_lock(&fr_atexit_global_mutex); + list->e = atexit_entry_alloc(NDEBUG_LOCATION_VALS + fr_atexit_threads, + _thread_local_free, + list); + + pthread_mutex_unlock(&fr_atexit_global_mutex); + + fr_atexit_thread_local = list; + } + + /* + * Now allocate the actual atexit handler entry + */ + if (atexit_entry_alloc(NDEBUG_LOCATION_VALS fr_atexit_thread_local, func, uctx) == NULL) return -1; + + return 0; +} + +/** Remove a specific destructor for this thread (without executing them) + * + * @note This function's primary purpose is to help diagnose issues with destructors + * from within a debugger. + * + * @param[in] uctx_scope Only process entries where the func and scope both match. + * @param[in] func Entries matching this function will be disarmed. + * @param[in] uctx associated with the entry. + * @return How many destructors were disarmed. + */ +unsigned int fr_atexit_thread_local_disarm(bool uctx_scope, fr_atexit_t func, void const *uctx) +{ + fr_atexit_entry_t *e = NULL; + unsigned int count = 0; + + if (!fr_atexit_thread_local) return -1; + + while ((e = fr_dlist_next(&fr_atexit_thread_local->head, e))) { + fr_atexit_entry_t *disarm; + + if ((e->func != func) || ((e->uctx != uctx) && uctx_scope)) continue; + + ATEXIT_DEBUG("%s - Thread %u disarming %p/%p func=%p, uctx=%p (alloced %s:%u)", + __FUNCTION__, + (unsigned int)pthread_self(), + fr_atexit_thread_local, e, e->func, e->uctx, e->file, e->line); + disarm = e; + e = fr_dlist_remove(&fr_atexit_thread_local->head, e); + talloc_set_destructor(disarm, NULL); + talloc_free(disarm); + + count++; + } + + return count; +} + +/** Remove all destructors for this thread (without executing them) + * + * @note This function's primary purpose is to help diagnose issues with destructors + * from within a debugger. + */ +void fr_atexit_thread_local_disarm_all(void) +{ + fr_atexit_entry_t *e = NULL; + + if (!fr_atexit_thread_local) return; + + while ((e = fr_dlist_pop_head(&fr_atexit_thread_local->head))) { + ATEXIT_DEBUG("%s - Thread %u disarming %p/%p func=%p, uctx=%p (alloced %s:%u)", + __FUNCTION__, + (unsigned int)pthread_self(), + fr_atexit_thread_local, e, e->func, e->uctx, e->file, e->line); + talloc_set_destructor(e, NULL); + talloc_free(e); + } +} + +/** Cause all thread local free triggers to fire + * + * This is necessary when we're running in single threaded mode + * to ensure all "thread-local" memory (which isn't actually thread local) + * is cleaned up. + * + * One example is the OpenSSL log BIOs which must be cleaned up + * before fr_openssl_free is called. + * + * @return + * - >= 0 The number of atexit handlers triggered on success. + * - <0 the return code from any atexit handlers that returned an error. + */ +int fr_atexit_thread_trigger_all(void) +{ + fr_atexit_entry_t *e = NULL, *ee, *to_free; + fr_atexit_list_t *list; + unsigned int count = 0; + + /* + * Iterate over the list of thread local + * destructor lists running the + * destructors. + */ + while ((e = fr_dlist_next(&fr_atexit_threads->head, e))) { + if (!e->func) continue; /* thread already joined */ + + list = talloc_get_type_abort(e->uctx, fr_atexit_list_t); + ee = NULL; + while ((ee = fr_dlist_next(&list->head, ee))) { + ATEXIT_DEBUG("%s - Thread %u triggering %p/%p func=%p, uctx=%p (alloced %s:%u)", + __FUNCTION__, + (unsigned int)pthread_self(), + list, ee, ee->func, ee->uctx, ee->file, ee->line); + + count++; + to_free = ee; + ee = fr_dlist_remove(&list->head, ee); + if (talloc_free(to_free) < 0) { + fr_strerror_printf_push("atexit handler failed %p/%p func=%p, uctx=%p" +#ifndef NDEBUG + " (alloced %s:%u)" +#endif + , + list, to_free, + to_free->func, to_free->uctx +#ifndef NDEBUG + , to_free->file, to_free->line +#endif + ); + return -1; + } + } + } + + return count; +} +#endif diff --git a/src/lib/util/atexit.h b/src/lib/util/atexit.h index 9d80f1ca25d..17c9f9da629 100644 --- a/src/lib/util/atexit.h +++ b/src/lib/util/atexit.h @@ -34,16 +34,12 @@ RCSIDH(atexit_h, "$Id$") extern "C" { #endif -/* - * Because GCC only added support in 2013 *sigh* - */ -#ifdef TLS_STORAGE_CLASS -# define _Thread_local TLS_STORAGE_CLASS -#endif - /** Destructor callback * * @param[in] uctx to free. + * @return + * - 0 on success. + * - -1 on failure. */ typedef int(*fr_atexit_t)(void *uctx); @@ -59,8 +55,25 @@ int _atexit_global(NDEBUG_LOCATION_ARGS fr_atexit_t func, void const *uctx); * - 0 on success. * - -1 on failure. */ -#define fr_atexit_global(_func, _uctx) \ - _atexit_global(NDEBUG_LOCATION_EXP _func, _uctx) +#define fr_atexit_global(_func, _uctx) _atexit_global(NDEBUG_LOCATION_EXP _func, _uctx) + +unsigned int fr_atexit_global_disarm(bool uctx_scope, fr_atexit_t func, void const *uctx); + +void fr_atexit_global_disarm_all(void); + +int fr_atexit_global_trigger_all(void); + +int fr_atexit_trigger(bool uctx_scope, fr_atexit_t func, void const *uctx); + +bool fr_atexit_is_exiting(void); + +#ifdef HAVE_PTHREADS +/* + * Because GCC only added support in 2013 *sigh* + */ +#ifdef TLS_STORAGE_CLASS +# define _Thread_local TLS_STORAGE_CLASS +#endif /** Setup pair of global init/free functions * @@ -82,17 +95,17 @@ int _atexit_global(NDEBUG_LOCATION_ARGS fr_atexit_t func, void const *uctx); { \ static atomic_bool _init_done = false; \ static pthread_mutex_t _init_mutex = PTHREAD_MUTEX_INITIALIZER; \ + void *_our_uctx = _uctx; /* stop _uctx being evaluated multiple times, it may be a call to malloc() */ \ if (unlikely(!atomic_load(&_init_done))) { \ pthread_mutex_lock(&_init_mutex); \ if (!atomic_load(&_init_done)) { \ - _init(_uctx); \ - fr_atexit_global(_free, _uctx); \ + _init(_our_uctx); \ + fr_atexit_global(_free, _our_uctx); \ atomic_store(&_init_done, true); \ } \ pthread_mutex_unlock(&_init_mutex); \ } \ } - /** Set a destructor for thread local storage to free the memory on thread exit * * @note Pointers to thread local storage seem to become unusable as threads are @@ -119,15 +132,41 @@ void fr_atexit_thread_local_disarm_all(void); int fr_atexit_thread_trigger_all(void); -unsigned int fr_atexit_global_disarm(bool uctx_scope, fr_atexit_t func, void const *uctx); - -void fr_atexit_global_disarm_all(void); - -int fr_atexit_global_trigger_all(void); - -int fr_atexit_trigger(bool uctx_scope, fr_atexit_t func, void const *uctx); - -bool fr_atexit_is_exiting(void); +/* + * If we're building without threading support, + * all this becomes much easier, and we just map + * all thread local cleanup entries to the global + * list. + */ +#else +/* + * Don't emit a _Thread_local_storage qualifier + */ +# define __Thread_local +# define fr_atexit_global_once(_init, _free, _uctx) \ +do { \ + static bool _init_done = false; \ + void * _our_uctx = _uctx; /* stop _uctx being evaluated multiple times, it may be a call to malloc() */ \ + if (unlikely(!_init_done)) { \ + _init(_our_uctx); \ + fr_atexit_global(_free, _our_uctx); \ + _init_done = true; \ + } \ +} while(0); +# define fr_atexit_thread_local(_name, _free, _uctx) \ +do { \ + static bool _init_done = false; \ + void * _our_uctx = _uctx; /* stop _uctx being evaluated multiple times, it may be a call to malloc() */ \ + if (unlikely(!_init_done)) { \ + fr_atexit_global(_free, _our_uctx); \ + _init_done = true; \ + } \ + _name = _our_uctx; \ +} while(0); +# define fr_atexit_thread_local_disarm(...) fr_atexit_global_disarm(__VA_ARGS__) +# define fr_atexit_thread_local_disarm_all(...) fr_atexit_global_disarm_all(__VA_ARGS__) +# define fr_atexit_thread_trigger_all(...) fr_atexit_global_trigger_all(__VA_ARGS__) +#endif #ifdef __cplusplus } diff --git a/src/lib/util/dl.c b/src/lib/util/dl.c index 67078037ef0..6be8069e2d6 100644 --- a/src/lib/util/dl.c +++ b/src/lib/util/dl.c @@ -857,7 +857,7 @@ dl_loader_t *dl_loader_init(TALLOC_CTX *ctx, void *uctx, bool uctx_free, bool de * emscripten, so keep this as a potentially * runtime toggle for now. */ -#ifdef __EMCRIPTEN__ +#ifdef __EMSCRIPTEN__ dl_loader->do_static = true; #else dl_loader->do_static = false;