[PATCH 6/6] elf: Use dl_scratch_buffer for LD_LIBRARY_PATH copy in _dl_init_paths
H.J. Lu
hjl.tools@gmail.com
Fri May 15 02:04:16 GMT 2026
On Thu, May 14, 2026 at 3:20 AM Adhemerval Zanella
<adhemerval.zanella@linaro.org> wrote:
>
> _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.
> ---
> elf/Makefile | 3 ++
> elf/dl-load.c | 53 ++++++++++++++++-----
> elf/tst-dl-llp-stack.c | 106 +++++++++++++++++++++++++++++++++++++++++
> 3 files changed, 150 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 fc5d9961ef4..7e64efe97cd 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..c05ea7ec275
> --- /dev/null
> +++ b/elf/tst-dl-llp-stack.c
> @@ -0,0 +1,106 @@
> +/* 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
> + (~64 KB of synthetic entries). */
> +
> +#include <getopt.h>
> +#include <stdlib.h>
> +#include <string.h>
> +#include <sys/resource.h>
> +#include <sys/wait.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 },
> +
> +/* 16 entries × 4000 bytes ≈ 64 KB of synthetic LD_LIBRARY_PATH. */
> +enum { llp_entry_len = 4000, llp_entries = 16 };
> +
> +/* Stack rlimit for the child. Tuned so that regular loader startup
> + (envp on the initial stack + a few KB of loader frames) fits. */
> +enum { stack_limit_kb = 128 };
> +
> +/* Build a "LD_LIBRARY_PATH=" string with llp_entries synthetic
> + colon-separated entries each llp_entry_len bytes long. */
> +static char *
> +build_llp_env (void)
> +{
> + size_t junk_len = (size_t) llp_entries * (1 + llp_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', llp_entry_len - 1);
> + p += llp_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];
> +
> + char *llp_env = build_llp_env ();
> + 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_kb * 1024;
> + 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);
> +
> + if (WIFSIGNALED (proc.status))
> + FAIL_EXIT1 ("child killed by signal %d", WTERMSIG (proc.status));
> + TEST_VERIFY_EXIT (WIFEXITED (proc.status));
> + TEST_COMPARE (WEXITSTATUS (proc.status), 0);
> +
> + support_capture_subprocess_free (&proc);
> + free (llp_env);
> + return 0;
> +}
> +
> +#define TEST_FUNCTION_ARGV do_test
> +#include <support/test-driver.c>
> --
> 2.43.0
>
LGTM.
Reviewed-by: H.J. Lu <hjl.tools@gmail.com>
Thanks.
--
H.J.
More information about the Libc-alpha
mailing list