[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