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

H.J. Lu hjl.tools@gmail.com
Fri May 15 02:03:45 GMT 2026


On Thu, May 14, 2026 at 3:20 AM Adhemerval Zanella
<adhemerval.zanella@linaro.org> wrote:
>
> 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);

Does it work for Hurd?

> +  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
>


-- 
H.J.


More information about the Libc-alpha mailing list