[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