[PATCH v2 5/5] elf: Use dl_scratch_buffer for LD_LIBRARY_PATH copy in _dl_init_paths

Adhemerval Zanella adhemerval.zanella@linaro.org
Fri May 15 18:21:51 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           |   3 ++
 elf/dl-load.c          |  53 ++++++++++++++-----
 elf/tst-dl-llp-stack.c | 112 +++++++++++++++++++++++++++++++++++++++++
 3 files changed, 156 insertions(+), 12 deletions(-)
 create mode 100644 elf/tst-dl-llp-stack.c

diff --git a/elf/Makefile b/elf/Makefile
index 443e29493c2..2a90eaaeb30 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 \
@@ -2789,6 +2790,8 @@ generated += tst-bz26577-mod.so
 $(objpfx)tst-bz26577-minstack: $(shared-thread-library)
 $(objpfx)tst-bz26577-minstack.out: $(objpfx)tst-bz26577-mod.so
 
+tst-dl-llp-stack-ARGS = -- $(host-test-program-cmd)
+
 $(objpfx)tst-unwind-ctor: $(objpfx)tst-unwind-ctor-lib.so
 LDLIBS-tst-unwind-ctor += $(libunwind)
 LDFLAGS-tst-unwind-ctor-lib.so = -Wl,--unresolved-symbols=ignore-all
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..c6f7b85c1e3
--- /dev/null
+++ b/elf/tst-dl-llp-stack.c
@@ -0,0 +1,112 @@
+/* Test that a long LD_LIBRARY_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, builds an envp with only a long LD_LIBRARY_PATH
+   (16 pages of synthetic entries).
+
+   The envp size and stack rlimit are both expressed in pages so the
+   test scales naturally across PAGE_SIZE values.  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/support.h>
+
+static int restart;
+#define CMDLINE_OPTIONS \
+  { "restart", no_argument, &restart, 1 },
+
+enum { llp_entries = 16 };
+
+/* Build a "LD_LIBRARY_PATH=" string with llp_entries synthetic
+   colon-separated entries, each one page long, so the total payload
+   is 16 * PAGE_SIZE bytes.  */
+static char *
+build_llp_env (size_t entry_len)
+{
+  size_t junk_len = (size_t) llp_entries * (1 + entry_len);
+  char *env = xmalloc (strlen ("LD_LIBRARY_PATH=") + junk_len + 1);
+  char *p = stpcpy (env, "LD_LIBRARY_PATH=");
+  for (int i = 0; i < llp_entries; i++)
+    {
+      *p++ = ':';
+      *p++ = '/';
+      memset (p, 'd', entry_len - 1);
+      p += entry_len - 1;
+    }
+  *p = '\0';
+  return env;
+}
+
+static int
+do_test (int argc, char *argv[])
+{
+  if (restart)
+    return 0;
+
+  TEST_VERIFY_EXIT (argc == 2 || argc == 5);
+  const char *test_binary = argv[argc - 1];
+
+  /* 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 llp_entry_len = (size_t) page_size - 1;
+  size_t stack_limit = (size_t) 24 * page_size;
+
+  char *llp_env = build_llp_env (llp_entry_len);
+  char *envp[] = { llp_env, 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);
+
+  char *child_argv[] = {
+    (char *) test_binary,
+    (char *) "--direct",
+    (char *) "--restart",
+    NULL
+  };
+  struct support_capture_subprocess proc
+    = support_capture_subprogram (test_binary, child_argv, 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 (llp_env);
+  return 0;
+}
+
+#define TEST_FUNCTION_ARGV do_test
+#include <support/test-driver.c>
-- 
2.43.0



More information about the Libc-alpha mailing list