[PATCH 5/6] elf: Use dl_scratch_buffer for DST expansion in _dl_map_object_deps

Adhemerval Zanella adhemerval.zanella@linaro.org
Wed May 13 19:18:23 GMT 2026


The expand_dst macro in _dl_map_object_deps performs an unbounded
alloca via DL_DST_REQUIRED, which scales with the link map's
l_origin length plus the count of dynamic-string tokens in the
input string.  When a DT_NEEDED entry carries several DSTs and the
link map sits in a deep directory, the resulting allocation grows
to several kilobytes -- enough to overflow a PTHREAD_STACK_MIN
thread that calls dlopen.

Convert the macro to a static function that draws from a caller-
owned dl_scratch_buffer, so oversized expansions land on the heap
(or anonymous mmap during early startup) instead of the stack.
The scratch buffer is reused across DT_NEEDED, DT_AUXILIARY, and
DT_FILTER entries of the same map and freed once dependency
expansion completes.

A new regression test, tst-dst-needed-minstack, builds a wrapper
library that inherits a five-DST SONAME from a leaf module,
deploys it under a deep temporary directory, and dlopens it from
a PTHREAD_STACK_MIN thread.  Without the fix the dlopen overflows
the thread stack and crashes; with the fix the dlopen returns
cleanly (with or without a successful load).

Checked on aarch64-linux-gnu, x86_64-linux-gnu, and i686-linux-gnu.
---
 elf/Makefile                  |   9 +++
 elf/dl-deps.c                 | 114 +++++++++++++++++++-------------
 elf/tst-dst-needed-leaf-mod.c |  20 ++++++
 elf/tst-dst-needed-minstack.c | 120 ++++++++++++++++++++++++++++++++++
 elf/tst-dst-needed-wrap-mod.c |  21 ++++++
 5 files changed, 238 insertions(+), 46 deletions(-)
 create mode 100644 elf/tst-dst-needed-leaf-mod.c
 create mode 100644 elf/tst-dst-needed-minstack.c
 create mode 100644 elf/tst-dst-needed-wrap-mod.c

diff --git a/elf/Makefile b/elf/Makefile
index 239d146566d..443e29493c2 100644
--- a/elf/Makefile
+++ b/elf/Makefile
@@ -434,6 +434,7 @@ tests += \
   tst-dlopenfail-2 \
   tst-dlopenrpath \
   tst-dlsym-error \
+  tst-dst-needed-minstack \
   tst-filterobj \
   tst-filterobj-dlopen \
   tst-glibc-hwcaps \
@@ -946,6 +947,8 @@ modules-names += \
   tst-dlopenfailmod3 \
   tst-dlopenfailnodelmod \
   tst-dlopenrpathmod \
+  tst-dst-needed-leaf-mod \
+  tst-dst-needed-wrap-mod \
   tst-filterobj-aux \
   tst-filterobj-filtee \
   tst-filterobj-flt \
@@ -2871,6 +2874,12 @@ $(objpfx)tst-ldconfig-ld_so_conf-update.out: $(objpfx)tst-ldconfig-ld-mod.so
 $(objpfx)tst-dl-cache-long-path: $(shared-thread-library)
 $(objpfx)tst-dl-cache-long-path.out: $(objpfx)tst-dl-path-buf-mod.so
 
+LDFLAGS-tst-dst-needed-leaf-mod.so = \
+  -Wl,-soname,\$$ORIGIN/\$$ORIGIN/\$$ORIGIN/\$$ORIGIN/\$$ORIGIN/leaf.so
+$(objpfx)tst-dst-needed-wrap-mod.so: $(objpfx)tst-dst-needed-leaf-mod.so
+$(objpfx)tst-dst-needed-minstack: $(shared-thread-library)
+$(objpfx)tst-dst-needed-minstack.out: $(objpfx)tst-dst-needed-wrap-mod.so
+
 LDFLAGS-tst-filterobj-flt.so = -Wl,--filter=$(objpfx)tst-filterobj-filtee.so
 $(objpfx)tst-filterobj: $(objpfx)tst-filterobj-flt.so
 $(objpfx)tst-filterobj.out: $(objpfx)tst-filterobj-filtee.so
diff --git a/elf/dl-deps.c b/elf/dl-deps.c
index 4c363180ce7..446163b55c8 100644
--- a/elf/dl-deps.c
+++ b/elf/dl-deps.c
@@ -30,6 +30,7 @@
 #include <scratch_buffer.h>
 
 #include <dl-dst.h>
+#include <dl-scratch-buffer.h>
 
 /* Whether an shared object references one or more auxiliary objects
    is signaled by the AUXTAG entry in l_info.  */
@@ -80,47 +81,35 @@ struct list
   };
 
 
-/* Macro to expand DST.  It is an macro since we use `alloca'.  */
-#define expand_dst(l, str, fatal) \
-  ({									      \
-    const char *__str = (str);						      \
-    const char *__result = __str;					      \
-    size_t __dst_cnt = _dl_dst_count (__str);				      \
-									      \
-    if (__dst_cnt != 0)							      \
-      {									      \
-	char *__newp;							      \
-									      \
-	/* DST must not appear in SUID/SGID programs.  */		      \
-	if (__libc_enable_secure)					      \
-	  _dl_signal_error (0, __str, NULL, N_("\
-DST not allowed in SUID/SGID programs"));				      \
-									      \
-	__newp = (char *) alloca (DL_DST_REQUIRED (l, __str, strlen (__str),  \
-						   __dst_cnt));		      \
-									      \
-	__result = _dl_dst_substitute (l, __str, __newp);		      \
-									      \
-	if (*__result == '\0')						      \
-	  {								      \
-	    /* The replacement for the DST is not known.  We can't	      \
-	       processed.  */						      \
-	    if (fatal)							      \
-	      _dl_signal_error (0, __str, NULL, N_("\
-empty dynamic string token substitution"));				      \
-	    else							      \
-	      {								      \
-		/* This is for DT_AUXILIARY.  */			      \
-		if (__glibc_unlikely (GLRO(dl_debug_mask) & DL_DEBUG_LIBS))   \
-		  _dl_debug_printf (N_("\
-cannot load auxiliary `%s' because of empty dynamic string token "	      \
-					    "substitution\n"), __str);	      \
-		continue;						      \
-	      }								      \
-	  }								      \
-      }									      \
-									      \
-    __result; })
+/* Expand the dynamic-string-tokens ($ORIGIN / $LIB / $PLATFORM) in INPUT
+   using L's context.  Returns the expanded string -- a pointer either into
+   SCRATCH->data (when expansion was needed) or back at INPUT (when no DSTs
+   were present, so no allocation happened).  Returns NULL when a DST was
+   present but could not be resolved.
+
+   SCRATCH must be an init'd dl_scratch_buffer the caller will release once
+   the returned string is no longer needed.  This function never returns when
+   called for a SUID/SGID that contains DSTs: it raises a loader error.  */
+static const char *
+expand_dst (struct link_map *l, const char *input,
+	    struct dl_scratch_buffer *scratch)
+{
+  size_t dst_cnt = _dl_dst_count (input);
+  if (dst_cnt == 0)
+    return input;
+
+  /* DST must not appear in SUID/SGID programs.  */
+  if (__libc_enable_secure)
+    _dl_signal_error (0, input, NULL, N_("\
+DST not allowed in SUID/SGID programs"));
+
+  size_t total = DL_DST_REQUIRED (l, input, strlen (input), dst_cnt);
+  dl_scratch_buffer_allocate (scratch, total + 1, 0);
+  const char *result = _dl_dst_substitute (l, input, scratch->data);
+  if (*result == '\0')
+    return NULL;
+  return result;
+}
 
 static void
 preload (struct list *known, unsigned int *nlist, struct link_map *map)
@@ -224,12 +213,26 @@ _dl_map_object_deps (struct link_map *map,
 		/* Map in the needed object.  */
 		struct link_map *dep;
 
-		/* Recognize DSTs.  */
-		name = expand_dst (l, strtab + d->d_un.d_val, 0);
+		/* Recognize DSTs.  Empty substitution for DT_NEEDED is
+		   non-fatal: log and skip this entry.  */
+		struct dl_scratch_buffer scratch
+		  = dl_scratch_buffer_init ();
+		name = expand_dst (l, strtab + d->d_un.d_val, &scratch);
+		if (__glibc_unlikely (name == NULL))
+		  {
+		    if (__glibc_unlikely (GLRO(dl_debug_mask) & DL_DEBUG_LIBS))
+		      _dl_debug_printf (N_("\
+cannot load auxiliary `%s' because of empty dynamic string token "
+					    "substitution\n"),
+					strtab + d->d_un.d_val);
+		    dl_scratch_buffer_free (&scratch);
+		    continue;
+		  }
 		/* Store the tag in the argument structure.  */
 		args.name = name;
 
 		int err = _dl_catch_exception (&exception, openaux, &args);
+		dl_scratch_buffer_free (&scratch);
 		if (__glibc_unlikely (exception.errstring != NULL))
 		  {
 		    if (err)
@@ -267,9 +270,25 @@ _dl_map_object_deps (struct link_map *map,
 	      {
 		struct list *newp;
 
-		/* Recognize DSTs.  */
-		name = expand_dst (l, strtab + d->d_un.d_val,
-				   d->d_tag == DT_AUXILIARY);
+		/* Recognize DSTs.  DT_AUXILIARY is fatal on unresolved
+		   DST; DT_FILTER is non-fatal and is skipped.  */
+		struct dl_scratch_buffer scratch
+		  = dl_scratch_buffer_init ();
+		name = expand_dst (l, strtab + d->d_un.d_val, &scratch);
+		if (__glibc_unlikely (name == NULL))
+		  {
+		    dl_scratch_buffer_free (&scratch);
+		    if (d->d_tag == DT_AUXILIARY)
+		      _dl_signal_error (0, strtab + d->d_un.d_val, NULL,
+					N_("empty dynamic string token "
+					   "substitution"));
+		    if (__glibc_unlikely (GLRO(dl_debug_mask) & DL_DEBUG_LIBS))
+		      _dl_debug_printf (N_("\
+cannot load auxiliary `%s' because of empty dynamic string token "
+					    "substitution\n"),
+					strtab + d->d_un.d_val);
+		    continue;
+		  }
 		/* Store the tag in the argument structure.  */
 		args.name = name;
 
@@ -285,6 +304,9 @@ _dl_map_object_deps (struct link_map *map,
 		   object is not available.  For filter objects the dependency
 		   must be available.  */
 		int err = _dl_catch_exception (&exception, openaux, &args);
+		/* NAME is consumed by openaux above; release the DST
+		   scratch buffer regardless of outcome.  */
+		dl_scratch_buffer_free (&scratch);
 		if (__glibc_unlikely (exception.errstring != NULL))
 		  {
 		    if (d->d_tag == DT_AUXILIARY)
diff --git a/elf/tst-dst-needed-leaf-mod.c b/elf/tst-dst-needed-leaf-mod.c
new file mode 100644
index 00000000000..30d756be399
--- /dev/null
+++ b/elf/tst-dst-needed-leaf-mod.c
@@ -0,0 +1,20 @@
+/* Leaf DSO whose SONAME contains several DST tokens.  Used by
+   tst-dst-needed-minstack via tst-dst-needed-wrap-mod.so.
+   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/>.  */
+
+int tst_dst_needed_leaf_dummy;
diff --git a/elf/tst-dst-needed-minstack.c b/elf/tst-dst-needed-minstack.c
new file mode 100644
index 00000000000..ab6f0aef796
--- /dev/null
+++ b/elf/tst-dst-needed-minstack.c
@@ -0,0 +1,120 @@
+/* Test that dlopen of a library whose DT_NEEDED string carries
+   several dynamic-string tokens does not overflow a
+   PTHREAD_STACK_MIN-sized thread.
+
+   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/>.  */
+
+/* The leaf library is linked with a SONAME containing five $DST tokens; the
+   wrapper library links against the leaf so its DT_NEEDED inherits that
+   string.  This test deploys the wrapper in a ~3 KB deep directory (so the
+   wrapper's l_origin matches), then dlopens it from a PTHREAD_STACK_MIN
+   thread.  The leaf is not actually reachable through the (impossible)
+   expanded path, so the dlopen is expected to fail -- the regression
+   assertion is that the failure occurs without a stack overflow.  */
+
+#include <dlfcn.h>
+#include <limits.h>
+#include <pthread.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/stat.h>
+
+#include <support/check.h>
+#include <support/support.h>
+#include <support/temp_file.h>
+#include <support/xstdio.h>
+#include <support/xthread.h>
+#include <support/xunistd.h>
+
+#define WRAP_MOD "tst-dst-needed-wrap-mod.so"
+
+/* Set by do_prepare; the absolute path of the wrapper as deployed in the
+   deep directory.  */
+static char *deep_wrap_path;
+
+/* Build <temp>/<long components>/ and return the final path; intermediates
+   are registered with add_temp_file so cleanup is automatic.  */
+static char *
+build_deep_directory (void)
+{
+  enum { component_len = 240, components = 14 };
+  char component[component_len + 1];
+  memset (component, 'd', component_len);
+  component[component_len] = '\0';
+
+  char *base = support_create_temp_directory ("tst-dst-needed-");
+  size_t cap = strlen (base) + components * (1 + component_len) + 1;
+  char *path = xmalloc (cap);
+  strcpy (path, base);
+  free (base);
+
+  for (int i = 0; i < components; ++i)
+    {
+      strcat (path, "/");
+      strcat (path, component);
+      xmkdirp (path, 0777);
+      add_temp_file (path);
+    }
+  return path;
+}
+
+static void
+do_prepare (int argc, char **argv)
+{
+  char *deep_dir = build_deep_directory ();
+  /* Deep enough that l_origin alone is well over PTHREAD_STACK_MIN.  */
+  TEST_VERIFY (strlen (deep_dir) > 256);
+
+  char *src = xasprintf ("%s/elf/" WRAP_MOD, support_objdir_root);
+  deep_wrap_path = xasprintf ("%s/" WRAP_MOD, deep_dir);
+  support_copy_file (src, deep_wrap_path);
+  add_temp_file (deep_wrap_path);
+  free (src);
+  free (deep_dir);
+}
+#define PREPARE do_prepare
+
+static void *
+minstack_thread (void *closure __attribute__ ((unused)))
+{
+  /* Trigger DT_NEEDED expansion on the deep wrapper.  The leaf's five-$DST
+     SONAME, expanded against the wrapper's deep l_origin, produces a buffer
+     of several KB inside _dl_map_object_deps.  We do not care whether the 
+     leaf is actually findable -- the test passes if and only if the dlopen 
+     returns without a stack overflow.  */
+  void *h = dlopen (deep_wrap_path, RTLD_NOW);
+  TEST_VERIFY_EXIT (h == NULL);
+  return NULL;
+}
+
+static int
+do_test (void)
+{
+  pthread_attr_t attr;
+  xpthread_attr_init (&attr);
+  xpthread_attr_setstacksize (&attr, PTHREAD_STACK_MIN);
+  pthread_t thr = xpthread_create (&attr, minstack_thread, NULL);
+  xpthread_join (thr);
+  xpthread_attr_destroy (&attr);
+
+  free (deep_wrap_path);
+  return 0;
+}
+
+#include <support/test-driver.c>
diff --git a/elf/tst-dst-needed-wrap-mod.c b/elf/tst-dst-needed-wrap-mod.c
new file mode 100644
index 00000000000..d6b95d8b119
--- /dev/null
+++ b/elf/tst-dst-needed-wrap-mod.c
@@ -0,0 +1,21 @@
+/* Wrapper DSO whose DT_NEEDED string inherits the DST-laden SONAME
+   of tst-dst-needed-leaf-mod.so.  Loading this from a
+   PTHREAD_STACK_MIN thread is what tst-dst-needed-minstack exercises.
+   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/>.  */
+
+int tst_dst_needed_wrap_dummy;
-- 
2.43.0



More information about the Libc-alpha mailing list