From: Adhemerval Zanella Date: Tue, 19 May 2026 13:23:55 +0000 (-0300) Subject: elf: Use dl_scratch_buffer for DST expansion in _dl_map_object_deps X-Git-Url: http://git.ipfire.org/gitweb/index.cgi?a=commitdiff_plain;h=3215ea5c45f0192af27a8fa5ffbc9b417f8cc72b;p=thirdparty%2Fglibc.git elf: Use dl_scratch_buffer for DST expansion in _dl_map_object_deps 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 --- diff --git a/elf/Makefile b/elf/Makefile index 00d1a558d8..5476dd84ab 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 \ @@ -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 diff --git a/elf/dl-deps.c b/elf/dl-deps.c index 4c363180ce..446163b55c 100644 --- a/elf/dl-deps.c +++ b/elf/dl-deps.c @@ -30,6 +30,7 @@ #include #include +#include /* 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 0000000000..30d756be39 --- /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 + . */ + +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 0000000000..d7fb7cd835 --- /dev/null +++ b/elf/tst-dst-needed-minstack.c @@ -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 + . */ + +/* 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 +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#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 // 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 diff --git a/elf/tst-dst-needed-wrap-mod.c b/elf/tst-dst-needed-wrap-mod.c new file mode 100644 index 0000000000..d6b95d8b11 --- /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 + . */ + +int tst_dst_needed_wrap_dummy;