[PATCH v2] elf: Don't crash in dlsym when tail-called from a constructor [BZ #34156]
Daan De Meyer
daan@amutable.com
Sun May 17 18:13:09 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. 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 <daan@amutable.com>
---
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(-)
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..8bc107f60f
--- /dev/null
+++ b/dlfcn/tst-dlsym-ctor.c
@@ -0,0 +1,36 @@
+/* Test that a tail-called dlsym from 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
+ 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..a45713ec28
--- /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. 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
--
2.54.0
More information about the Libc-alpha
mailing list