[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