[glibc] elf: Use dl_scratch_buffer for LD_LIBRARY_PATH copy in _dl_init_paths
Adhemerval Zanella via Glibc-cvs <[email protected]> Wed, 20 May 2026 18:18:13 +0000 (GMT)
| Newsgroups | gmane.comp.lib.glibc.cvs |
|---|---|
| Message-ID | <[email protected]> |
https://sourceware.org/git/gitweb.cgi?p=glibc.git;h=999fa30b3616beed08da30ab25971860ab9116ea commit 999fa30b3616beed08da30ab25971860ab9116ea Author: Adhemerval Zanella <[email protected]> Date: Tue May 19 10:23:56 2026 -0300 elf: Use dl_scratch_buffer for LD_LIBRARY_PATH copy in _dl_init_paths _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 <[email protected]> Diff: --- elf/Makefile | 1 + elf/dl-load.c | 53 +++++++++++++---- elf/tst-dl-llp-stack.c | 152 +++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 194 insertions(+), 12 deletions(-) diff --git a/elf/Makefile b/elf/Makefile index 5476dd84ab..aef13b73ca 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 204faffaa0..95404adae9 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 0000000000..fcfe478c72 --- /dev/null +++ b/elf/tst-dl-llp-stack.c @@ -0,0 +1,152 @@ +/* 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). + + Some kernels enforce that exec's argv+envp does not exceed RLIMIT_STACK/4; + the constraints here (envp+envp_copy must exceed RLIMIT_STACK) mean the + scenario cannot be set up on such kernels, in which case posix_spawn + returns E2BIG and the test is marked UNSUPPORTED. */ + +#include <errno.h> +#include <getopt.h> +#include <signal.h> +#include <spawn.h> +#include <stdlib.h> +#include <string.h> +#include <sys/resource.h> +#include <sys/wait.h> +#include <unistd.h> + +#include <support/check.h> +#include <support/subprocess.h> +#include <support/support.h> +#include <support/xunistd.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); + + pid_t pid; + int spawn_ret = posix_spawn (&pid, w->path, NULL, NULL, child_argv, + (char *const *) w->envp); + + TEST_VERIFY_EXIT (setrlimit (RLIMIT_STACK, &rl_save) == 0); + + if (spawn_ret == E2BIG) + FAIL_UNSUPPORTED ("posix_spawn returned E2BIG: this kernel enforces " + "argv+envp <= RLIMIT_STACK/4"); + + if (spawn_ret != 0) + { + errno = spawn_ret; + FAIL_EXIT1 ("posix_spawn: %m"); + } + + int status; + TEST_COMPARE (xwaitpid (pid, &status, 0), pid); + if (WIFSIGNALED (status)) + FAIL_EXIT1 ("child killed by signal %d", WTERMSIG (status)); + TEST_VERIFY_EXIT (WIFEXITED (status)); + TEST_COMPARE (WEXITSTATUS (status), 0); + + free (child_argv); + free (long_paths); + support_spawn_wrapped_free (w); + free (binary); + return 0; +} + +#include <support/test-driver.c>