[glibc] elf: Replace alloca with dl_scratch_buffer in _dl_load_cache_lookup

Adhemerval Zanella via Glibc-cvs <[email protected]> Wed, 20 May 2026 18:18:03 +0000 (GMT)
Newsgroups gmane.comp.lib.glibc.cvs
Message-ID <[email protected]>
https://sourceware.org/git/gitweb.cgi?p=glibc.git;h=3e3b621910d4d7981ed4a88d8f0c8a4ecf26becc

commit 3e3b621910d4d7981ed4a88d8f0c8a4ecf26becc
Author: Adhemerval Zanella <[email protected]>
Date:   Tue May 19 10:23:54 2026 -0300

    elf: Replace alloca with dl_scratch_buffer in _dl_load_cache_lookup
    
    The alloca added by commit ccdb048d ("Fix recursive dlopen") to
    snapshot the matched cache entry before __strdup runs through
    interposable malloc is sized by best_len, which can reach PATH_MAX.
    On PTHREAD_STACK_MIN threads that's enough to overflow the stack
    mid-dlopen.
    
    Use dl_scratch_buffer with DL_SCRATCH_NO_MALLOC: short entries stay
    in the 256-byte inline area, longer ones spill to anonymous mmap
    rather than to interposable malloc.  The recursive-dlopen invariant
    is preserved.
    
    New container test elf/tst-dl-cache-long-path constructs a ~3.4 KB
    deep directory, populates ld.so.cache with that entry, and dlopens
    from a PTHREAD_STACK_MIN thread under deliberate stack pressure;
    reliably SIGSEGVs against the alloca-based code and passes with the
    fix.
    
    Checked on aarch64-linux-gnu, x86_64-linux-gnu, and i686-linux-gnu.
    
    Reviewed-by: H.J. Lu <[email protected]>

Diff:
---
 elf/Makefile                 |   5 ++
 elf/dl-cache.c               |  14 ++--
 elf/tst-dl-cache-long-path.c | 174 +++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 188 insertions(+), 5 deletions(-)

diff --git a/elf/Makefile b/elf/Makefile
index f668dec368..00d1a558d8 100644
--- a/elf/Makefile
+++ b/elf/Makefile
@@ -293,6 +293,7 @@ CRT-tst-tls1-static-non-pie := $(csu-objpfx)crt1.o
 tst-tls1-static-non-pie-no-pie = yes
 
 tests-container := \
+  tst-dl-cache-long-path \
   tst-ldconfig-bad-aux-cache \
   tst-ldconfig-ld_so_conf-update \
   # tests-container
@@ -2886,6 +2887,10 @@ LDFLAGS-tst-dlopen-nodelete-reloc-mod17.so = -Wl,--no-as-needed
 
 $(objpfx)tst-ldconfig-ld_so_conf-update.out: $(objpfx)tst-ldconfig-ld-mod.so
 
+# Reuses the trivial module already built for tst-dl-path-buf.
+$(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-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-cache.c b/elf/dl-cache.c
index 9458ffae2a..c1de93f204 100644
--- a/elf/dl-cache.c
+++ b/elf/dl-cache.c
@@ -21,6 +21,7 @@
 #include <ldsodefs.h>
 #include <sys/mman.h>
 #include <dl-cache.h>
+#include <dl-scratch-buffer.h>
 #include <stdint.h>
 #include <_itoa.h>
 #include <dl-hwcaps.h>
@@ -490,12 +491,15 @@ _dl_load_cache_lookup (const char *name)
   /* The double copy is *required* since malloc may be interposed
      and call dlopen itself whose completion would unmap the data
      we are accessing. Therefore we must make the copy of the
-     mapping data without using malloc.  */
-  char *temp;
+     mapping data without using malloc.  The DL_SCRATCH_NO_MALLOC
+     forces any spill to anonymous mmap rather than the malloc.  */
+  struct dl_scratch_buffer scratch = dl_scratch_buffer_init ();
   size_t best_len = strlen (best) + 1;
-  temp = alloca (best_len);
-  memcpy (temp, best, best_len);
-  return __strdup (temp);
+  dl_scratch_buffer_allocate (&scratch, best_len, DL_SCRATCH_NO_MALLOC);
+  memcpy (scratch.data, best, best_len);
+  char *result = __strdup (scratch.data);
+  dl_scratch_buffer_free (&scratch);
+  return result;
 }
 
 #ifndef MAP_COPY
diff --git a/elf/tst-dl-cache-long-path.c b/elf/tst-dl-cache-long-path.c
new file mode 100644
index 0000000000..e730493422
--- /dev/null
+++ b/elf/tst-dl-cache-long-path.c
@@ -0,0 +1,174 @@
+/* Test dlopen through ld.so.cache with a cache entry longer than the
+   dl_scratch_buffer inline area.
+   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/>.  */
+
+/* This test populates the cache with a single library sitting in a directory
+   whose absolute path is far larger than the dl_scratch_buffer inline area
+   (256 bytes) and most of the way to PATH_MAX; the loader's cache lookup
+   therefore exercises the anonymous-mmap spill.  The dlopen is also repeated
+   from a PTHREAD_STACK_MIN thread to demonstrate that the path no longer
+   consumes too much caller's stack.  */
+
+#include <dlfcn.h>
+#include <limits.h>
+#include <pthread.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/stat.h>
+
+#include <support/capture_subprocess.h>
+#include <support/check.h>
+#include <support/support.h>
+#include <support/temp_file.h>
+#include <support/xdlfcn.h>
+#include <support/xstdio.h>
+#include <support/xthread.h>
+#include <support/xunistd.h>
+
+/* ldconfig only indexes filenames starting with "lib", so the module is
+   deployed under the lib-prefixed name (MOD_DEPLOYED) in the deep directory
+   and dlopened by that name.  */
+#define MOD_BUILT     "tst-dl-path-buf-mod.so"
+#define MOD_DEPLOYED  "libtst-dl-path-buf-mod.so"
+#define MOD_SYMBOL    "tst_dl_path_buf_mod_value"
+#define MOD_EXPECTED  0xaabbccddu
+
+/* Final absolute path of the deep directory holding the module; filled in by
+   setup ().  Kept around so dlopen_module can sanity-print it on failure.  */
+static char *deep_dir;
+
+static void
+run_ldconfig (void *x)
+{
+  char *prog = xasprintf ("%s/ldconfig", support_install_rootsbindir);
+  char *args[] = { prog, NULL };
+  execv (args[0], args);
+  FAIL_EXIT1 ("execv (%s): %m", prog);
+}
+
+/* Build /tst-dl-cache-long-path/d.../d.../d... with several long components,
+   totalling well past dl_scratch_buffer's inline area
+   (DL_SCRATCH_BUFFER_INLINE_SIZE = 256 bytes) and close to PATH_MAX.  */
+static char *
+build_deep_directory (void)
+{
+  enum { component_len = 250, components = 15 };
+  /* 14 * (1 + 240) = 3374 bytes of nesting, plus the base.  */
+  char component[component_len + 1];
+  memset (component, 'd', component_len);
+  component[component_len] = '\0';
+
+  const char *base = "/tst-dl-cache-long-path";
+  size_t cap = strlen (base) + components * (1 + component_len) + 1;
+  char *path = xmalloc (cap);
+  strcpy (path, base);
+  xmkdirp (path, 0777);
+  add_temp_file (path);
+
+  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)
+{
+  deep_dir = build_deep_directory ();
+  TEST_VERIFY (strlen (deep_dir) > 256);
+
+  char *src = xasprintf ("%s/elf/" MOD_BUILT, support_objdir_root);
+  char *dst = xasprintf ("%s/" MOD_DEPLOYED, deep_dir);
+  support_copy_file (src, dst);
+  add_temp_file (dst);
+  free (src);
+  free (dst);
+
+  char *conf = xasprintf ("%s/ld.so.conf", support_sysconfdir_prefix);
+  FILE *fp = xfopen (conf, "w");
+  fprintf (fp, "%s\n", deep_dir);
+  xfclose (fp);
+  free (conf);
+
+  xmkdirp ("/var/cache/ldconfig", 0777);
+  struct support_capture_subprocess r
+    = support_capture_subprocess (run_ldconfig, NULL);
+  support_capture_subprocess_check (&r, "ldconfig", 0, sc_allow_none);
+  support_capture_subprocess_free (&r);
+}
+#define PREPARE do_prepare
+
+static void
+__attribute_noinline__
+dlopen_via_cache (volatile char *pressure)
+{
+  if (pressure != NULL)
+    (void) *pressure;
+
+  void *h = xdlopen (MOD_DEPLOYED, RTLD_NOW | RTLD_LOCAL);
+  unsigned int (*fn) (void) = xdlsym (h, MOD_SYMBOL);
+  TEST_COMPARE (fn (), MOD_EXPECTED);
+  xdlclose (h);
+}
+
+/* Reduce the stack budget available to the dlopen call chain by
+   STACK_PRESSURE bytes.  */
+enum { STACK_PRESSURE = 5 * 1024 };
+
+static void
+__attribute_noinline__
+dlopen_via_cache_under_pressure (void)
+{
+  char filler[STACK_PRESSURE];
+  dlopen_via_cache (&filler[0]);
+}
+
+static void *
+minstack_thread (void *closure)
+{
+  dlopen_via_cache_under_pressure ();
+  return NULL;
+}
+
+static int
+do_test (void)
+{
+  /* Sanity: from the main thread (no stack pressure needed).  */
+  dlopen_via_cache (NULL);
+
+  /* The motivating scenario: from a PTHREAD_STACK_MIN thread.  Before
+     _dl_load_cache_lookup was converted to dl_scratch_buffer this would
+     have alloca'd ~3 KB mid-dlopen and risked overflowing.  */
+  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_dir);
+  return 0;
+}
+
+#include <support/test-driver.c>