[PATCH v3 6/6] elf: Use dl_scratch_buffer for LD_LIBRARY_PATH copy in _dl_init_paths

Adhemerval Zanella adhemerval.zanella@linaro.org
Tue May 19 13:23:56 GMT 2026


_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 <hjl.tools@gmail.com>
---
 elf/Makefile           |   1 +
 elf/dl-load.c          |  53 +++++++++++++----
 elf/tst-dl-llp-stack.c | 129 +++++++++++++++++++++++++++++++++++++++++
 3 files changed, 171 insertions(+), 12 deletions(-)
 create mode 100644 elf/tst-dl-llp-stack.c

diff --git a/elf/Makefile b/elf/Makefile
index 5476dd84abe..aef13b73ca5 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 204faffaa0b..95404adae94 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 00000000000..2fe85acd535
--- /dev/null
+++ b/elf/tst-dl-llp-stack.c
@@ -0,0 +1,129 @@
+/* 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).  */
+
+#include <getopt.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/resource.h>
+#include <unistd.h>
+
+#include <support/capture_subprocess.h>
+#include <support/check.h>
+#include <support/subprocess.h>
+#include <support/support.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);
+
+  struct support_capture_subprocess proc
+    = support_capture_subprogram (w->path, child_argv,
+				  (char *const *) w->envp);
+
+  TEST_VERIFY_EXIT (setrlimit (RLIMIT_STACK, &rl_save) == 0);
+
+  support_capture_subprocess_check (&proc, "tst-dl-llp-stack", 0,
+				    sc_allow_none);
+  support_capture_subprocess_free (&proc);
+  free (child_argv);
+  free (long_paths);
+  support_spawn_wrapped_free (w);
+  free (binary);
+  return 0;
+}
+
+#include <support/test-driver.c>
-- 
2.43.0



More information about the Libc-alpha mailing list