]> git.ipfire.org Git - thirdparty/glibc.git/commitdiff
elf: Use dl_scratch_buffer for DST expansion in _dl_map_object_deps
authorAdhemerval Zanella <adhemerval.zanella@linaro.org>
Tue, 19 May 2026 13:23:55 +0000 (10:23 -0300)
committerAdhemerval Zanella <adhemerval.zanella@linaro.org>
Wed, 20 May 2026 14:49:17 +0000 (11:49 -0300)
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.

Reviewed-by: H.J. Lu <hjl.tools@gmail.com>
elf/Makefile
elf/dl-deps.c
elf/tst-dst-needed-leaf-mod.c [new file with mode: 0644]
elf/tst-dst-needed-minstack.c [new file with mode: 0644]
elf/tst-dst-needed-wrap-mod.c [new file with mode: 0644]

index 00d1a558d89ba11673ebfed85a475c66650dc59c..5476dd84abe768bdfe6862b429bde23e70b21618 100644 (file)
@@ -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 \
@@ -2891,6 +2894,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
index 4c363180ce7d088eb98cbda46183c5045f33d087..446163b55c8abc6079505c5ccf4dd9c689bbb065 100644 (file)
@@ -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 (file)
index 0000000..30d756b
--- /dev/null
@@ -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 (file)
index 0000000..d7fb7cd
--- /dev/null
@@ -0,0 +1,122 @@
+/* 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)
+{
+  /* 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)
+{
+  size_t stacksize = support_small_thread_stack_size (true);
+
+  pthread_attr_t attr;
+  xpthread_attr_init (&attr);
+  xpthread_attr_setstacksize (&attr, stacksize);
+  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 (file)
index 0000000..d6b95d8
--- /dev/null
@@ -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;