[glibc] elf: Don't crash in dlsym when tail-called from a constructor [BZ #34156]

Adhemerval Zanella via Glibc-cvs <[email protected]> Wed, 20 May 2026 20:22:58 +0000 (GMT)
Newsgroups gmane.comp.lib.glibc.cvs
Message-ID <[email protected]>
https://sourceware.org/git/gitweb.cgi?p=glibc.git;h=994a4378785e3c2187d1b44e5e98d97eb2369882

commit 994a4378785e3c2187d1b44e5e98d97eb2369882
Author: Daan De Meyer <[email protected]>
Date:   Mon May 18 07:39:33 2026 +0000

    elf: Don't crash in dlsym when tail-called from a constructor [BZ #34156]
    
    If a shared library's constructor calls dlsym and discards the result,
    the compiler is free to lower the call to a tail jump.  The dynamic
    linker then resolves the apparent caller to ld.so's own link map, which
    has no l_scope, and crashes in _dl_lookup_symbol_x dereferencing the
    NULL scope pointer.
    
    Tail-call optimization is a legal C transformation and there is no way
    for the dynamic linker to recover the real caller from the elided frame.
    Detect the situation by its observable effect -- a link map with no
    l_scope -- and fall back to the main program's link map, the same
    treatment used when the caller's address is otherwise unrecognized.
    
    The check is written against l->l_scope rather than against _dl_rtld_map
    directly because dl-sym-post.h is also compiled into libc.so, where
    _dl_rtld_map is not visible (it lives only in ld.so).
    
    Add dlfcn/tst-dlsym-ctor exercising the pattern.  Without the fix the
    test SIGSEGVs during dlopen; with the fix dlopen returns cleanly.
    
    Signed-off-by: Daan De Meyer <[email protected]>
    Reviewed-by: Adhemerval Zanella  <[email protected]>

Diff:
---
 dlfcn/Makefile            |  4 ++++
 dlfcn/tst-dlsym-ctor.c    | 36 ++++++++++++++++++++++++++++++++++++
 dlfcn/tst-dlsym-ctormod.c | 28 ++++++++++++++++++++++++++++
 elf/dl-sym-post.h         | 14 +++++++++-----
 4 files changed, 77 insertions(+), 5 deletions(-)

diff --git a/dlfcn/Makefile b/dlfcn/Makefile
index 00341dd476..be4bd9cbe3 100644
--- a/dlfcn/Makefile
+++ b/dlfcn/Makefile
@@ -67,6 +67,7 @@ tests = \
   glrefmain \
   tst-dladdr \
   tst-dlinfo \
+  tst-dlsym-ctor \
   tst-rec-dlopen \
   tstatexit \
   tstcxaatexit \
@@ -90,6 +91,7 @@ modules-names = \
   modcxaatexit \
   moddummy1 \
   moddummy2 \
+  tst-dlsym-ctormod \
   # modules-names
 
 failtestmod.so-no-z-defs = yes
@@ -197,3 +199,5 @@ $(objpfx)bug-dl-leaf.out: $(objpfx)bug-dl-leaf-lib-cb.so
 $(objpfx)bug-dl-leaf-lib-cb.so: $(objpfx)bug-dl-leaf-lib.so
 
 $(objpfx)tst-rec-dlopen.out: $(objpfx)moddummy1.so $(objpfx)moddummy2.so
+
+$(objpfx)tst-dlsym-ctor.out: $(objpfx)tst-dlsym-ctormod.so
diff --git a/dlfcn/tst-dlsym-ctor.c b/dlfcn/tst-dlsym-ctor.c
new file mode 100644
index 0000000000..948980ce2e
--- /dev/null
+++ b/dlfcn/tst-dlsym-ctor.c
@@ -0,0 +1,36 @@
+/* Test that a tail-called dlsym from a constructor works.
+   Copyright The GNU Toolchain Authors.
+   This file is part of the GNU C Library.
+
+   The GNU C Library is free software; you can redistribute it and/or
+   modify it under the terms of the GNU Lesser General Public
+   License as published by the Free Software Foundation; either
+   version 2.1 of the License, or (at your option) any later version.
+
+   The GNU C Library is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+   Lesser General Public License for more details.
+
+   You should have received a copy of the GNU Lesser General Public
+   License along with the GNU C Library; if not, see
+   <https://www.gnu.org/licenses/>.  */
+
+#include <dlfcn.h>
+#include <support/check.h>
+#include <support/xdlfcn.h>
+
+static int
+do_test (void)
+{
+  /* Loading the module runs its constructor, which performs a dlsym
+     whose result is discarded.  Under optimization the compiler lowers
+     that to a tail call, so dlsym sees a caller address inside the
+     dynamic linker itself.  Before the fix that resolved to the ld.so
+     link map, which has no l_scope, and the lookup crashed.  */
+  void *h = xdlopen ("tst-dlsym-ctormod.so", RTLD_NOW);
+  xdlclose (h);
+  return 0;
+}
+
+#include <support/test-driver.c>
diff --git a/dlfcn/tst-dlsym-ctormod.c b/dlfcn/tst-dlsym-ctormod.c
new file mode 100644
index 0000000000..8aeb149e8a
--- /dev/null
+++ b/dlfcn/tst-dlsym-ctormod.c
@@ -0,0 +1,28 @@
+/* Module for tst-dlsym-ctor.
+   Copyright The GNU Toolchain Authors.
+   This file is part of the GNU C Library.
+
+   The GNU C Library is free software; you can redistribute it and/or
+   modify it under the terms of the GNU Lesser General Public
+   License as published by the Free Software Foundation; either
+   version 2.1 of the License, or (at your option) any later version.
+
+   The GNU C Library is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+   Lesser General Public License for more details.
+
+   You should have received a copy of the GNU Lesser General Public
+   License along with the GNU C Library; if not, see
+   <https://www.gnu.org/licenses/>.  */
+
+#include <dlfcn.h>
+
+/* The result is intentionally discarded so the compiler can lower the
+   dlsym call to a tail call.  That is the trigger for the bug -- see
+   tst-dlsym-ctor.c.  */
+__attribute__ ((constructor)) static void
+ctor (void)
+{
+  (void) dlsym (RTLD_DEFAULT, "tst_dlsym_ctor_no_such_symbol");
+}
diff --git a/elf/dl-sym-post.h b/elf/dl-sym-post.h
index 8f45298cc9..31c607defa 100644
--- a/elf/dl-sym-post.h
+++ b/elf/dl-sym-post.h
@@ -22,12 +22,16 @@ static struct link_map *
 _dl_sym_find_caller_link_map (ElfW(Addr) caller)
 {
   struct link_map *l = _dl_find_dso_for_object (caller);
-  if (l != NULL)
+  /* A constructor that tail-calls dlsym makes the caller address point
+     into the dynamic linker itself.  The ld.so link map has no l_scope
+     set, so using it for a symbol lookup would dereference NULL.  Treat
+     that like an unknown caller.  */
+  if (l != NULL && l->l_scope != NULL)
     return l;
-  else
-    /* If the address is not recognized the call comes from the main
-       program (we hope).  */
-    return GL(dl_ns)[LM_ID_BASE]._ns_loaded;
+  /* The address does not belong to any loaded object (e.g. it is in
+     JIT-generated code or in the main program).  Fall back to the main
+     program's link map.  */
+  return GL(dl_ns)[LM_ID_BASE]._ns_loaded;
 }
 
 /* Translates RESULT, *REF, VALUE into a symbol address from the point