[PATCH RESEND 3/3] elf: allow dl_iterate_phdr() to be called concurrently

Caleb Sander Mateos csander@purestorage.com
Tue Sep 23 19:27:04 GMT 2025


dl_iterate_phdr() internally acquires the dl_load_write_lock mutex,
which means it will block if called concurrently on a second thread.
This mutual exclusion is surprising for a function that's read-only in
spirit. And it can cause deadlocks in applications calling it from
multiple places if the user-supplied callback acquires a nested mutex.
For example, an application compiled with ThreadSanitizer that uses
dl_iterate_phdr() to build a symbol table can deadlock if TSAN detects a
race condition concurrently. This is because any memory access inside
the dl_iterate_phdr() callback may be intercepted by TSAN and block on a
TSAN-internal semaphore. At the same time, TSAN's race reporting may
already hold the semaphore and call into dl_iterate_phdr() to symbolize
the stacktrace, blocking on dl_load_write_lock.

Change dl_load_write_lock from a mutex to a reader-writer lock to allow
concurrent calls to dl_iterate_phdr(). _dl_add_to_namespace_list() and
_dl_close_worker() acquire a write lock as they modify the loaded object
list, while __dl_iterate_phdr() acquires a read lock as it only reads
from the list.

Example backtraces from threads in the dl_iterate_phdr-TSAN deadlock:
0  0x00000000043dacee in __sanitizer::FutexWait(__sanitizer::atomic_uint32_t*, unsigned int) ()
1  0x00000000043dc23a in __sanitizer::Semaphore::Wait() ()
2  0x000000000446cf90 in __tsan::TraceSwitchPartImpl(__tsan::ThreadState*) ()
3  0x000000000446eadd in void __tsan::RestartMemoryAccessRange<true>(__tsan::ThreadState*, unsigned long, unsigned long, unsigned long) ()
4  0x000000000440d7ab in __interceptor_strcmp ()
5  0x00000000068bb33f in elf_add (state=0x7f8270031000, filename=filename@entry=0x7f8270777de0 "/lib64/libresolv.so.2", descriptor=42, memory=memory@entry=0x0, memory_size=memory_size@entry=0, base_address=140198202634240, error_callback=0x4c3abe2 <backtrace_error_cb(void*, char const*, int)>, data=0x7f822a965e60, fileline_fn=0x7f822a965b48, found_sym=0x7f822a965d34, found_dwarf=0x7f822a965b54, fileline_entry=0x0, exe=0, debuginfo=0, with_buildid_data=0x0, with_buildid_size=0, so_filename=0x7f8270777de0 "/lib64/libresolv.so.2") at ./tpc/backtrace/elf.c:4264
6  0x00000000068bd405 in phdr_callback (info=0x7f822a965be0, size=<optimized out>, pdata=0x7f822a965cd0) at ./tpc/backtrace/elf.c:4896
7  0x000000000440cbe2 in dl_iterate_phdr_cb(__sanitizer::__sanitizer_dl_phdr_info*, unsigned long, void*) ()
8  0x00007f826fb56874 in __GI___dl_iterate_phdr (callback=0x440caf0 <dl_iterate_phdr_cb(__sanitizer::__sanitizer_dl_phdr_info*, unsigned long, void*)>, data=0x7f822a965c88) at dl-iteratephdr.c:75
9  0x000000000440ca52 in __interceptor_dl_iterate_phdr ()
10 0x00000000068ba9d2 in backtrace_initialize (state=state@entry=0x7f8270031000, filename=filename@entry=0x30c2219 <.L.str.189> "/proc/self/exe", descriptor=<optimized out>, error_callback=error_callback@entry=0x4c3abe2 <backtrace_error_cb(void*, char const*, int)>, data=data@entry=0x7f822a965e60, fileline_fn=fileline_fn@entry=0x7f822a965db8) at ./tpc/backtrace/elf.c:4940

0  futex_wait (private=0, expected=2, futex_word=0x7f827093a9b0 <_rtld_local+2480>) at ../sysdeps/nptl/futex-internal.h:146
1  __GI___lll_lock_wait (futex=futex@entry=0x7f827093a9b0 <_rtld_local+2480>, private=0) at lowlevellock.c:50
2  0x00007f826fa8d5ad in lll_mutex_lock_optimized (mutex=0x7f827093a9b0 <_rtld_local+2480>) at pthread_mutex_lock.c:49
3  ___pthread_mutex_lock (mutex=0x7f827093a9b0 <_rtld_local+2480>) at pthread_mutex_lock.c:129
4  0x00007f826fb5671c in __GI___dl_iterate_phdr (callback=0x43e6ae0 <__sanitizer::dl_iterate_phdr_cb(dl_phdr_info*, unsigned long, void*)>, data=0x7f822a8c34f0) at dl-iteratephdr.c:40
5  0x000000000440c9cd in __interceptor_dl_iterate_phdr ()
6  0x00000000043e6acd in __sanitizer::ListOfModules::init() ()
7  0x00000000043f0645 in __sanitizer::Symbolizer::FindModuleForAddress(unsigned long) ()
8  0x00000000043f083b in __sanitizer::Symbolizer::SymbolizeData(unsigned long, __sanitizer::DataInfo*) ()
9  0x0000000004482486 in __tsan::SymbolizeData(unsigned long) ()
10 0x000000000447d457 in __tsan::ScopedReportBase::AddLocation(unsigned long, unsigned long) ()
11 0x000000000447f82f in __tsan::ReportRace(__tsan::ThreadState*, __tsan::RawShadow*, __tsan::Shadow, __tsan::Shadow, unsigned long) ()

Signed-off-by: Caleb Sander Mateos <csander@purestorage.com>
---
 elf/dl-close.c             | 4 ++--
 elf/dl-iteratephdr.c       | 6 +++---
 elf/dl-object.c            | 4 ++--
 elf/dl-support.c           | 2 +-
 elf/rtld.c                 | 2 +-
 sysdeps/generic/ldsodefs.h | 2 +-
 6 files changed, 10 insertions(+), 10 deletions(-)

diff --git a/elf/dl-close.c b/elf/dl-close.c
index 83e4f012b2..11cffffb9b 100644
--- a/elf/dl-close.c
+++ b/elf/dl-close.c
@@ -482,11 +482,11 @@ _dl_close_worker (struct link_map *map, bool force)
 
   /* Protects global and module specitic TLS state.  */
   __rtld_lock_lock_recursive (GL(dl_load_tls_lock));
 
   /* We modify the list of loaded objects.  */
-  __rtld_lock_lock_recursive (GL(dl_load_write_lock));
+  __rtld_rwlock_wrlock_recursive (GL(dl_load_write_lock));
 
   /* Check each element of the search list to see if all references to
      it are gone.  */
   for (unsigned int i = first_loaded; i < nloaded; ++i)
     {
@@ -704,11 +704,11 @@ _dl_close_worker (struct link_map *map, bool force)
 
 	  free (imap);
 	}
     }
 
-  __rtld_lock_unlock_recursive (GL(dl_load_write_lock));
+  __rtld_rwlock_unlock_recursive (GL(dl_load_write_lock));
 
   /* If we removed any object which uses TLS bump the generation counter.  */
   if (any_tls)
     {
       size_t newgen = GL(dl_tls_generation) + 1;
diff --git a/elf/dl-iteratephdr.c b/elf/dl-iteratephdr.c
index e83e7e2a0c..dfe029acd1 100644
--- a/elf/dl-iteratephdr.c
+++ b/elf/dl-iteratephdr.c
@@ -22,11 +22,11 @@
 #include <libc-lock.h>
 
 static void
 cancel_handler (void *arg __attribute__((unused)))
 {
-  __rtld_lock_unlock_recursive (GL(dl_load_write_lock));
+  __rtld_rwlock_unlock_recursive (GL(dl_load_write_lock));
 }
 
 int
 __dl_iterate_phdr (int (*callback) (struct dl_phdr_info *info,
 				    size_t size, void *data), void *data)
@@ -34,11 +34,11 @@ __dl_iterate_phdr (int (*callback) (struct dl_phdr_info *info,
   struct link_map *l;
   struct dl_phdr_info info;
   int ret = 0;
 
   /* Make sure nobody modifies the list of loaded objects.  */
-  __rtld_lock_lock_recursive (GL(dl_load_write_lock));
+  __rtld_rwlock_rdlock_recursive (GL(dl_load_write_lock));
   __libc_cleanup_push (cancel_handler, NULL);
 
   /* We have to determine the namespace of the caller since this determines
      which namespace is reported.  */
   size_t nloaded = GL(dl_ns)[0]._ns_nloaded;
@@ -76,11 +76,11 @@ __dl_iterate_phdr (int (*callback) (struct dl_phdr_info *info,
 	break;
     }
 
   /* Release the lock.  */
   __libc_cleanup_pop (0);
-  __rtld_lock_unlock_recursive (GL(dl_load_write_lock));
+  __rtld_rwlock_unlock_recursive (GL(dl_load_write_lock));
 
   return ret;
 }
 hidden_def (__dl_iterate_phdr)
 
diff --git a/elf/dl-object.c b/elf/dl-object.c
index 51d3704edc..0f5133a207 100644
--- a/elf/dl-object.c
+++ b/elf/dl-object.c
@@ -28,11 +28,11 @@
 /* Add the new link_map NEW to the end of the namespace list.  */
 void
 _dl_add_to_namespace_list (struct link_map *new, Lmid_t nsid)
 {
   /* We modify the list of loaded objects.  */
-  __rtld_lock_lock_recursive (GL(dl_load_write_lock));
+  __rtld_rwlock_wrlock_recursive (GL(dl_load_write_lock));
 
   if (GL(dl_ns)[nsid]._ns_loaded != NULL)
     {
       struct link_map *l = GL(dl_ns)[nsid]._ns_loaded;
       while (l->l_next != NULL)
@@ -45,11 +45,11 @@ _dl_add_to_namespace_list (struct link_map *new, Lmid_t nsid)
     GL(dl_ns)[nsid]._ns_loaded = new;
   ++GL(dl_ns)[nsid]._ns_nloaded;
   new->l_serial = GL(dl_load_adds);
   ++GL(dl_load_adds);
 
-  __rtld_lock_unlock_recursive (GL(dl_load_write_lock));
+  __rtld_rwlock_unlock_recursive (GL(dl_load_write_lock));
 }
 
 
 /* Allocate a `struct link_map' for a new object being loaded,
    and enter it into the _dl_loaded list.  */
diff --git a/elf/dl-support.c b/elf/dl-support.c
index bbef3ab6b6..edf033839c 100644
--- a/elf/dl-support.c
+++ b/elf/dl-support.c
@@ -207,11 +207,11 @@ struct link_map *_dl_sysinfo_map;
    At this time it is not anymore a problem to modify the tables.  */
 __rtld_lock_define_initialized_recursive (, _dl_load_lock)
 /* This lock is used to keep __dl_iterate_phdr from inspecting the
    list of loaded objects while an object is added to or removed from
    that list.  */
-__rtld_lock_define_initialized_recursive (, _dl_load_write_lock)
+__rtld_rwlock_define_initialized_recursive (, _dl_load_write_lock)
   /* This lock protects global and module specific TLS related data.
      E.g. it is held in dlopen and dlclose when GL(dl_tls_generation),
      GL(dl_tls_max_dtv_idx) or GL(dl_tls_dtv_slotinfo_list) are
      accessed and when TLS related relocations are processed for a
      module.  It was introduced to keep pthread_create accessing TLS
diff --git a/elf/rtld.c b/elf/rtld.c
index 753ce6690b..60e6421a58 100644
--- a/elf/rtld.c
+++ b/elf/rtld.c
@@ -323,11 +323,11 @@ struct rtld_global _rtld_global =
     /* Generally the default presumption without further information is an
      * executable stack but this is not true for all platforms.  */
     ._dl_stack_prot_flags = DEFAULT_STACK_PROT_PERMS,
 #ifdef _LIBC_REENTRANT
     ._dl_load_lock = _RTLD_LOCK_RECURSIVE_INITIALIZER,
-    ._dl_load_write_lock = _RTLD_LOCK_RECURSIVE_INITIALIZER,
+    ._dl_load_write_lock = _RTLD_RWLOCK_RECURSIVE_INITIALIZER,
     ._dl_load_tls_lock = _RTLD_LOCK_RECURSIVE_INITIALIZER,
 #endif
     ._dl_nns = 1,
     ._dl_ns =
     {
diff --git a/sysdeps/generic/ldsodefs.h b/sysdeps/generic/ldsodefs.h
index f36e64ddba..6e78e18882 100644
--- a/sysdeps/generic/ldsodefs.h
+++ b/sysdeps/generic/ldsodefs.h
@@ -368,11 +368,11 @@ struct rtld_global
      At this time it is not anymore a problem to modify the tables.  */
   __rtld_lock_define_recursive (EXTERN, _dl_load_lock)
   /* This lock is used to keep __dl_iterate_phdr from inspecting the
      list of loaded objects while an object is added to or removed
      from that list.  */
-  __rtld_lock_define_recursive (EXTERN, _dl_load_write_lock)
+  __rtld_rwlock_define_recursive (EXTERN, _dl_load_write_lock)
   /* This lock protects global and module specific TLS related data.
      E.g. it is held in dlopen and dlclose when GL(dl_tls_generation),
      GL(dl_tls_max_dtv_idx) or GL(dl_tls_dtv_slotinfo_list) are
      accessed and when TLS related relocations are processed for a
      module.  It was introduced to keep pthread_create accessing TLS
-- 
2.43.0



More information about the Libc-alpha mailing list