[PATCH v3] dlfcn: Fix dlclose crash in atexit handler after thread_local destructor (BZ 33598)

Adhemerval Zanella Netto adhemerval.zanella@linaro.org
Tue May 5 20:55:17 GMT 2026



On 05/05/26 17:45, Adhemerval Zanella Netto wrote:
> 
> 
> On 05/05/26 16:36, Florian Weimer wrote:
>> * Adhemerval Zanella:
>>
>>> @@ -46,6 +47,9 @@ __run_exit_handlers (int status, struct exit_function_list **listp,
>>>    /* The exit should never return, so there is no need to unlock it.  */
>>>    __libc_lock_lock_recursive (__exit_lock);
>>>  
>>> +  /* Disable unmap objects through dlclose by TLS destructor (BZ 33598).  */
>>> +  GL(dl_at_exit) = true;
>>> +
>>>    /* First, call the TLS destructors.  */
>>>    if (run_dtors)
>>>      call_function_static_weak (__call_tls_dtors);
>>> diff --git a/sysdeps/generic/ldsodefs.h b/sysdeps/generic/ldsodefs.h
>>> index 15c46598539..236419c593c 100644
>>> --- a/sysdeps/generic/ldsodefs.h
>>> +++ b/sysdeps/generic/ldsodefs.h
>>> @@ -441,6 +441,9 @@ struct rtld_global
>>>    /* Generation counter for the dtv.  */
>>>    EXTERN size_t _dl_tls_generation;
>>>  
>>> +  /* Disable unmap objects during __run_exit_handlers.  */
>>> +  EXTERN bool _dl_at_exit;
>>> +
>>>    /* Scopes to free after next THREAD_GSCOPE_WAIT ().  */
>>>    EXTERN struct dl_scope_free_list
>>>    {
>>
>> Would it be simpler to do what the comment says?  Just guard the
>> munmap call that is part of dlclose?  This could even be considered an
>> optimization.
> 
> It should work, the main difference is with this approach (assume_in_use = false),
> _dl_close_worker during exit marks every object IDX_STILL_USED and returns 
> immediately, so fo finalizers run. The _dl_fini handles every library in its
> own sorted pass later.
> 
> With the DL_UNMAP approach, _dl_close_worker runs normally and unloadable libraries
> get their finalizers called by _dl_call_fini inside _dl_close_worker.
> 
> I think it makes sense to use your suggestion. I will send a new version.

However, analyzing this a bit more I am not sure. With DL_UNMAP the link map is 
still removed from the namespace, but the pages remain mapped and the DSO is gone
from GL(dl_ns[nsid]._ns_loaded), _dl_loaded_lock, and l_initfini chains. This 
creates an inconsistent state where:

* This approach guarantees that _dl_fini runs all fini/fini_array callbacks in a
  single topologically-sorted pass, where DL_UNMAP _dl_close_worker's _dl_call_fini 
  may run destructors for a library during exit, and then _dl_fini may encounter 
  it again (or not, if the link map was removed).

* dl_iterate_phdr and dladdr — used by profilers and alike (ASan/TSan) will miss 
  libraries whose pages are still live.

* LD_AUDIT (la_objclose callbacks) fires for each dlclose during exit. 

This approach makes exit-time dlclose semantically a no-op, although a bit more
complex.

So I leaning a bit more for the proposed approach.



More information about the Libc-alpha mailing list