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

Daan De Meyer daan@amutable.com
Sat May 16 22:15:45 GMT 2026


If a shared library's constructor calls dlsym() and discards the result,
the compiler is free to lower the call to a tail jump (jmp dlsym@plt
instead of call+ret).  When dlsym() then reads __builtin_return_address
(0) to identify its caller it walks past the elided frame and lands
inside call_init() in ld.so.  _dl_sym_find_caller_link_map then resolves
that address to the dynamic linker's own link map (_dl_rtld_map), which
has no l_scope, and _dl_lookup_symbol_x SIGSEGVs 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 <daan@amutable.com>
---
 dlfcn/Makefile            |  4 ++++
 dlfcn/tst-dlsym-ctor.c    | 38 ++++++++++++++++++++++++++++++++++++++
 dlfcn/tst-dlsym-ctormod.c | 28 ++++++++++++++++++++++++++++
 elf/dl-sym-post.h         | 15 ++++++++++-----
 4 files changed, 80 insertions(+), 5 deletions(-)
 create mode 100644 dlfcn/tst-dlsym-ctor.c
 create mode 100644 dlfcn/tst-dlsym-ctormod.c

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..44164ea456
--- /dev/null
+++ b/dlfcn/tst-dlsym-ctor.c
@@ -0,0 +1,38 @@
+/* Test that dlsym() from a tail-called position in a constructor works.
+   Copyright (C) 2026 Free Software Foundation, Inc.
+   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(RTLD_DEFAULT, ...) whose result is discarded.  Under -O2 the
+     compiler emits a tail call to dlsym, making __builtin_return_address(0)
+     inside the public dlsym() wrapper read through the elided frame and
+     land inside call_init() in ld.so.  Before the fix, the dynamic linker
+     then used the dynamic linker's own link_map (which has no l_scope) for
+     the lookup and crashed in _dl_lookup_symbol_x.  */
+  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..dc824aadb4
--- /dev/null
+++ b/dlfcn/tst-dlsym-ctormod.c
@@ -0,0 +1,28 @@
+/* Module for tst-dlsym-ctor.
+   Copyright (C) 2026 Free Software Foundation, Inc.
+   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 (jmp dlsym@plt instead of call+ret).  That is
+   the trigger condition 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..514f5e623f 100644
--- a/elf/dl-sym-post.h
+++ b/elf/dl-sym-post.h
@@ -22,12 +22,17 @@ 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)
+  /* If the resolved caller has no usable lookup scope, the real caller is
+     a constructor that tail-called dlsym(): __builtin_return_address(0)
+     in the public dlsym() wrapper read through the elided frame and landed
+     inside call_init() in ld.so, which has no l_scope.  Using that link
+     map for a lookup would deref NULL.  Treat it like an unknown caller
+     and fall back to the main program's link map.  */
+  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;
+  /* If the address is not recognized the call comes from the main
+     program (we hope).  */
+  return GL(dl_ns)[LM_ID_BASE]._ns_loaded;
 }
 
 /* Translates RESULT, *REF, VALUE into a symbol address from the point
-- 
2.54.0



More information about the Libc-alpha mailing list