[glibc] elf: Use dl_scratch_buffer for LD_LIBRARY_PATH copy in _dl_init_paths

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

commit 999fa30b3616beed08da30ab25971860ab9116ea
Author: Adhemerval Zanella <[email protected]>
Date:   Tue May 19 10:23:56 2026 -0300

    elf: Use dl_scratch_buffer for LD_LIBRARY_PATH copy in _dl_init_paths
    
    _dl_init_paths used strdupa to make a mutable copy of LD_LIBRARY_PATH
    for fillin_rpath to tokenize.  The env block is attacker-controllable
    and Linux allows individual variables up to MAX_ARG_STRLEN (32 *
    PAGE_SIZE = 128 KB), so the strdupa can push tens of KB onto the
    loader's startup stack on top of the env block that already sits on
    the initial stack.  With a reduced RLIMIT_STACK the doubled copy
    overflows before main () is reached.
    
    Replace the strdupa with a dl_scratch_buffer: short paths stay in
    the 256-byte inline area, longer ones spill to anonymous mmap (malloc
    is not yet available during _dl_init_paths).  Two follow-on changes
    make the new scratch lifetime safe against _dl_signal_error:
    
      * Count entries directly off the const LD_LIBRARY_PATH and allocate
        __rtld_env_path_list.dirs *before* the scratch is live.  That way
        the larger of the two heap allocations the loader controls signals
        its OOM with no scratch to leak.
    
      * Convert fillin_rpath to return bool instead of calling
        _dl_signal_error internally on per-entry malloc failure.  Its
        only caller in the LLP path now frees the scratch first and then
        signals the error from a clean state.  decompose_rpath, the other
        caller, is updated symmetrically.  This also fixes a pre-existing
        leak in fillin_rpath's OOM path, where the to_free heap copy from
        expand_dynamic_string_token was not released before the
        _dl_signal_error.
    
    Checked on x86_64-linux-gnu, aarch64-linux-gnu, and i686-linux-gnu.
    
    Reviewed-by: H.J. Lu <[email protected]>

Diff:
---
 elf/Makefile           |   1 +
 elf/dl-load.c          |  53 +++++++++++++----
 elf/tst-dl-llp-stack.c | 152 +++++++++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 194 insertions(+), 12 deletions(-)

diff --git a/elf/Makefile b/elf/Makefile
index 5476dd84ab..aef13b73ca 100644
--- a/elf/Makefile
+++ b/elf/Makefile
@@ -411,6 +411,7 @@ tests += \
   tst-debug1 \
   tst-deep1 \
   tst-dl-is_dso \
+  tst-dl-llp-stack \
   tst-dl-path-buf \
   tst-dlclose-lazy \
   tst-dlmodcount \
diff --git a/elf/dl-load.c b/elf/dl-load.c
index 204faffaa0..95404adae9 100644
--- a/elf/dl-load.c
+++ b/elf/dl-load.c
@@ -422,7 +422,10 @@ struct r_search_path_struct __rtld_search_dirs attribute_relro;
 
 static size_t max_dirnamelen;
 
-static struct r_search_path_elem **
+/* Tokenize RPATH (in place) and populate RESULT with one entry per non-empty
+   directory.  Returns false if a per-entry allocation fails, leaving the
+   caller responsible for signaling any error.  */
+static bool
 fillin_rpath (char *rpath, struct r_search_path_elem **result, const char *sep,
 	      const char *what, const char *where, struct link_map *l)
 {
@@ -490,8 +493,10 @@ fillin_rpath (char *rpath, struct r_search_path_elem **result, const char *sep,
 	    malloc (sizeof (*dirp) + ncapstr * sizeof (enum r_dir_status)
 		    + where_len + len + 1);
 	  if (dirp == NULL)
-	    _dl_signal_error (ENOMEM, NULL, NULL,
-			      N_("cannot create cache for search path"));
+	    {
+	      free (to_free);
+	      return false;
+	    }
 
 	  dirp->dirname = ((char *) dirp + sizeof (*dirp)
 			   + ncapstr * sizeof (enum r_dir_status));
@@ -528,7 +533,7 @@ fillin_rpath (char *rpath, struct r_search_path_elem **result, const char *sep,
   /* Terminate the array.  */
   result[nelems] = NULL;
 
-  return result;
+  return true;
 }
 
 
@@ -609,7 +614,13 @@ decompose_rpath (struct r_search_path_struct *sps,
       _dl_signal_error (ENOMEM, NULL, NULL, errstring);
     }
 
-  fillin_rpath (copy, result, ":", what, where, l);
+  if (!fillin_rpath (copy, result, ":", what, where, l))
+    {
+      free (copy);
+      free (result);
+      errstring = N_("cannot create cache for search path");
+      goto signal_error;
+    }
 
   /* Free the copied RPATH string.  `fillin_rpath' make own copies if
      necessary.  */
@@ -782,12 +793,13 @@ _dl_init_paths (const char *llp, const char *source,
 
   if (llp != NULL && *llp != '\0')
     {
-      char *llp_tmp = strdupa (llp);
-
-      /* Decompose the LD_LIBRARY_PATH contents.  First determine how many
-	 elements it has.  */
+      /* Count entries directly off the const LD_LIBRARY_PATH so the
+	 search-path dirs array can be allocated before the scratch buffer is
+	 live; that way an OOM on either of the two heap allocations the
+	 loader controls (the dirs array or the per-entry malloc inside
+	 fillin_rpath) is signalled after the scratch has been released.  */
       size_t nllp = 1;
-      for (const char *cp = llp_tmp; *cp != '\0'; ++cp)
+      for (const char *cp = llp; *cp != '\0'; ++cp)
 	if (*cp == ':' || *cp == ';')
 	  ++nllp;
 
@@ -799,8 +811,25 @@ _dl_init_paths (const char *llp, const char *source,
 	  goto signal_error;
 	}
 
-      (void) fillin_rpath (llp_tmp, __rtld_env_path_list.dirs, ":;",
-			   source, NULL, l);
+      /* fillin_rpath needs a mutable copy because __strsep punches NULs
+	 into it as it tokenizes.  */
+      size_t llp_len = strlen (llp);
+      struct dl_scratch_buffer scratch = dl_scratch_buffer_init ();
+      dl_scratch_buffer_allocate (&scratch, llp_len + 1, 0);
+      char *llp_tmp = memcpy (scratch.data, llp, llp_len + 1);
+
+      bool ok = fillin_rpath (llp_tmp, __rtld_env_path_list.dirs, ":;",
+			      source, NULL, l);
+
+      dl_scratch_buffer_free (&scratch);
+
+      if (!ok)
+	{
+	  free (__rtld_env_path_list.dirs);
+	  __rtld_env_path_list.dirs = NULL;
+	  errstring = N_("cannot create cache for search path");
+	  goto signal_error;
+	}
 
       if (__rtld_env_path_list.dirs[0] == NULL)
 	{
diff --git a/elf/tst-dl-llp-stack.c b/elf/tst-dl-llp-stack.c
new file mode 100644
index 0000000000..fcfe478c72
--- /dev/null
+++ b/elf/tst-dl-llp-stack.c
@@ -0,0 +1,152 @@
+/* Test that a long search path does not overflow loader startup stack.
+   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 reduces RLIMIT_STACK to a value that just covers regular loader
+   startup, then spawns a child whose dynamic-linker search path is
+   artificially long (16 pages of synthetic colon-separated entries).
+
+   On exec the kernel places argv+envp at the top of the initial stack and
+   rounds the reservation to page granularity, which would otherwise eat the
+   loader's entire budget on architectures with large pages (e.g. 64 KB-page
+   aarch64).
+
+   Some kernels enforce that exec's argv+envp does not exceed RLIMIT_STACK/4;
+   the constraints here (envp+envp_copy must exceed RLIMIT_STACK) mean the
+   scenario cannot be set up on such kernels, in which case posix_spawn
+   returns E2BIG and the test is marked UNSUPPORTED.  */
+
+#include <errno.h>
+#include <getopt.h>
+#include <signal.h>
+#include <spawn.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/resource.h>
+#include <sys/wait.h>
+#include <unistd.h>
+
+#include <support/check.h>
+#include <support/subprocess.h>
+#include <support/support.h>
+#include <support/xunistd.h>
+
+static int restart;
+#define CMDLINE_OPTIONS \
+  { "restart", no_argument, &restart, 1 },
+
+enum { llp_entries = 16 };
+
+/* Return a newly malloc'd string of the form "PREFIX[:/ddd...]{16}"
+   where each synthetic entry is ENTRY_LEN bytes long.  PREFIX is the
+   ld.so --library-path value from support_spawn_wrap and supplies the
+   real build directories so the rtld can still resolve libc.  */
+static char *
+build_long_library_path (const char *prefix, size_t entry_len)
+{
+  size_t prefix_len = strlen (prefix);
+  size_t junk_len = (size_t) llp_entries * (1 + entry_len);
+  char *out = xmalloc (prefix_len + junk_len + 1);
+  char *p = stpcpy (out, prefix);
+  for (int i = 0; i < llp_entries; i++)
+    {
+      *p++ = ':';
+      *p++ = '/';
+      memset (p, 'd', entry_len - 1);
+      p += entry_len - 1;
+    }
+  *p = '\0';
+  return out;
+}
+
+static int
+do_test (void)
+{
+  if (restart)
+    return 0;
+
+  char *binary = xasprintf ("%s/elf/tst-dl-llp-stack", support_objdir_root);
+  char *child_argv_in[] = {
+    binary, (char *) "--direct", (char *) "--restart", NULL
+  };
+
+  struct support_spawn_wrapped *w
+    = support_spawn_wrap (binary, child_argv_in, NULL,
+			  support_spawn_wrap_force);
+
+  /* Scale envp and stack rlimit with PAGE_SIZE to handle kernels with
+     different page sizes.  */
+  long page_size = sysconf (_SC_PAGESIZE);
+  TEST_VERIFY_EXIT (page_size > 0);
+  size_t entry_len = (size_t) page_size - 1;
+  size_t stack_limit = (size_t) 24 * page_size;
+
+  /* Extend the wrapped --library-path value with the synthetic junk.
+     Build a fresh argv whose slot 2 points at our long-paths string;
+     all other slots alias into the wrapped argv, which keeps ownership
+     of those strings.  */
+  TEST_VERIFY_EXIT (w->argv[0] != NULL && w->argv[1] != NULL
+		    && w->argv[2] != NULL
+		    && strcmp (w->argv[1], "--library-path") == 0);
+  char *long_paths = build_long_library_path (w->argv[2], entry_len);
+
+  size_t nargs;
+  for (nargs = 0; w->argv[nargs] != NULL; nargs++)
+    ;
+  char **child_argv = xcalloc (nargs + 1, sizeof (*child_argv));
+  for (size_t i = 0; i < nargs; i++)
+    child_argv[i] = (i == 2) ? long_paths : (char *) w->argv[i];
+  child_argv[nargs] = NULL;
+
+  /* Reduce the stack rlimit; the posix_spawn'd child inherits it.  */
+  struct rlimit rl_save, rl_small;
+  TEST_VERIFY_EXIT (getrlimit (RLIMIT_STACK, &rl_save) == 0);
+  rl_small.rlim_cur = (rlim_t) stack_limit;
+  rl_small.rlim_max = rl_save.rlim_max;
+  TEST_VERIFY_EXIT (setrlimit (RLIMIT_STACK, &rl_small) == 0);
+
+  pid_t pid;
+  int spawn_ret = posix_spawn (&pid, w->path, NULL, NULL, child_argv,
+			       (char *const *) w->envp);
+
+  TEST_VERIFY_EXIT (setrlimit (RLIMIT_STACK, &rl_save) == 0);
+
+  if (spawn_ret == E2BIG)
+    FAIL_UNSUPPORTED ("posix_spawn returned E2BIG: this kernel enforces "
+		      "argv+envp <= RLIMIT_STACK/4");
+
+  if (spawn_ret != 0)
+    {
+      errno = spawn_ret;
+      FAIL_EXIT1 ("posix_spawn: %m");
+    }
+
+  int status;
+  TEST_COMPARE (xwaitpid (pid, &status, 0), pid);
+  if (WIFSIGNALED (status))
+    FAIL_EXIT1 ("child killed by signal %d", WTERMSIG (status));
+  TEST_VERIFY_EXIT (WIFEXITED (status));
+  TEST_COMPARE (WEXITSTATUS (status), 0);
+
+  free (child_argv);
+  free (long_paths);
+  support_spawn_wrapped_free (w);
+  free (binary);
+  return 0;
+}
+
+#include <support/test-driver.c>