[v4,10/14] elf: Bootstrap allocation for future protected memory allocator
Checks
| Context |
Check |
Description |
| redhat-pt-bot/TryBot-apply_patch |
success
|
Patch applied to master at the time it was sent
|
| linaro-tcwg-bot/tcwg_glibc_build--master-aarch64 |
success
|
Build passed
|
| linaro-tcwg-bot/tcwg_glibc_build--master-arm |
success
|
Build passed
|
| linaro-tcwg-bot/tcwg_glibc_check--master-aarch64 |
fail
|
Test failed
|
| linaro-tcwg-bot/tcwg_glibc_check--master-arm |
fail
|
Test failed
|
Commit Message
A subsequent change will place link maps into memory which is
read-only most of the time. This means that the link map for
ld.so itself (GLPM (dl_rtld_map)) needs to be put there as well,
which requires allocating it dynamically.
---
elf/Makefile | 1 +
elf/dl-protmem_bootstrap.h | 29 ++++
elf/rtld.c | 87 ++++++----
elf/tst-rtld-nomem.c | 177 ++++++++++++++++++++
sysdeps/generic/dl-early_mmap.h | 35 ++++
sysdeps/generic/ldsodefs.h | 6 +-
sysdeps/mips/Makefile | 6 +
sysdeps/unix/sysv/linux/dl-early_allocate.c | 17 +-
sysdeps/unix/sysv/linux/dl-early_mmap.h | 41 +++++
9 files changed, 345 insertions(+), 54 deletions(-)
create mode 100644 elf/dl-protmem_bootstrap.h
create mode 100644 elf/tst-rtld-nomem.c
create mode 100644 sysdeps/generic/dl-early_mmap.h
create mode 100644 sysdeps/unix/sysv/linux/dl-early_mmap.h
Comments
On 02/02/25 18:13, Florian Weimer wrote:
> A subsequent change will place link maps into memory which is
> read-only most of the time. This means that the link map for
> ld.so itself (GLPM (dl_rtld_map)) needs to be put there as well,
> which requires allocating it dynamically.
Is an extra mmap on each program invocation, with all the extra error
handling complexity (which it requires to move the handling later in
self-relocation), really worth here?
Maybe we can either define a supported maximum page (similar to your
work on "Teach glibc about possible page sizes and handle gaps in ld.so"
patchset), or get if from ld thorough configure; and use it to define
both alignment and size for the rtld_protmem. The ld can either later
move it to relro or apply read-only protection itself.
> ---
> elf/Makefile | 1 +
> elf/dl-protmem_bootstrap.h | 29 ++++
> elf/rtld.c | 87 ++++++----
> elf/tst-rtld-nomem.c | 177 ++++++++++++++++++++
> sysdeps/generic/dl-early_mmap.h | 35 ++++
> sysdeps/generic/ldsodefs.h | 6 +-
> sysdeps/mips/Makefile | 6 +
> sysdeps/unix/sysv/linux/dl-early_allocate.c | 17 +-
> sysdeps/unix/sysv/linux/dl-early_mmap.h | 41 +++++
> 9 files changed, 345 insertions(+), 54 deletions(-)
> create mode 100644 elf/dl-protmem_bootstrap.h
> create mode 100644 elf/tst-rtld-nomem.c
> create mode 100644 sysdeps/generic/dl-early_mmap.h
> create mode 100644 sysdeps/unix/sysv/linux/dl-early_mmap.h
>
> diff --git a/elf/Makefile b/elf/Makefile
> index 5c833871d0..1d93993241 100644
> --- a/elf/Makefile
> +++ b/elf/Makefile
> @@ -463,6 +463,7 @@ tests += \
> tst-rtld-no-malloc \
> tst-rtld-no-malloc-audit \
> tst-rtld-no-malloc-preload \
> + tst-rtld-nomem \
> tst-rtld-run-static \
> tst-single_threaded \
> tst-single_threaded-pthread \
> diff --git a/elf/dl-protmem_bootstrap.h b/elf/dl-protmem_bootstrap.h
> new file mode 100644
> index 0000000000..a2fc267a2d
> --- /dev/null
> +++ b/elf/dl-protmem_bootstrap.h
> @@ -0,0 +1,29 @@
> +/* Bootstrap allocation for the protected memory area.
> + Copyright (C) 2025 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/>. */
> +
> +#include <dl-early_mmap.h>
> +
> +/* Return a pointer to the protected memory area, or NULL if
> + allocation fails. This function is called before self-relocation,
> + and the system call needs to be inlined for (most)
> + HIDDEN_VAR_NEEDS_DYNAMIC_RELOC targets. */
> +static inline __attribute__ ((always_inline)) struct rtld_protmem *
> +_dl_protmem_bootstrap (void)
> +{
> + return _dl_early_mmap (sizeof (struct rtld_protmem));
> +}
> diff --git a/elf/rtld.c b/elf/rtld.c
> index 0bc7d9dbcd..de9e87cd0b 100644
> --- a/elf/rtld.c
> +++ b/elf/rtld.c
> @@ -53,6 +53,7 @@
> #include <dl-find_object.h>
> #include <dl-audit-check.h>
> #include <dl-call_tls_init_tp.h>
> +#include <dl-protmem_bootstrap.h>
>
> #include <assert.h>
>
> @@ -345,8 +346,6 @@ struct rtld_global _rtld_global =
> extern struct rtld_global _rtld_local
> __attribute__ ((alias ("_rtld_global"), visibility ("hidden")));
>
> -struct rtld_protmem _rtld_protmem;
> -
> /* This variable is similar to _rtld_local, but all values are
> read-only after relocation. */
> struct rtld_global_ro _rtld_global_ro attribute_relro =
> @@ -421,6 +420,7 @@ static ElfW(Addr) _dl_start_final (void *arg);
> struct dl_start_final_info
> {
> struct link_map l;
> + struct rtld_protmem *protmem;
> RTLD_TIMING_VAR (start_time);
> };
> static ElfW(Addr) _dl_start_final (void *arg,
> @@ -455,6 +455,14 @@ _dl_start_final (void *arg, struct dl_start_final_info *info)
> {
> ElfW(Addr) start_addr;
>
> +#ifndef DONT_USE_BOOTSTRAP_MAP
> + GLRO (dl_protmem) = info->protmem;
> +#endif
> +
> + /* Delayed error reporting after relocation processing. */
> + if (GLRO (dl_protmem) == NULL)
> + _dl_fatal_printf ("Fatal glibc error: Cannot allocate link map\n");
> +
> __rtld_malloc_init_stubs ();
>
> /* Do not use an initializer for these members because it would
> @@ -478,21 +486,10 @@ _dl_start_final (void *arg, struct dl_start_final_info *info)
> RTLD_TIMING_SET (start_time, info->start_time);
> #endif
>
> - /* Transfer data about ourselves to the permanent link_map structure. */
> -#ifndef DONT_USE_BOOTSTRAP_MAP
> - GLPM(dl_rtld_map).l_addr = info->l.l_addr;
> - GLPM(dl_rtld_map).l_ld = info->l.l_ld;
> - GLPM(dl_rtld_map).l_ld_readonly = info->l.l_ld_readonly;
> - memcpy (GLPM(dl_rtld_map).l_info, info->l.l_info,
> - sizeof GLPM(dl_rtld_map).l_info);
> - GLPM(dl_rtld_map).l_mach = info->l.l_mach;
> - GLPM(dl_rtld_map).l_relocated = 1;
> -#endif
> _dl_setup_hash (&GLPM(dl_rtld_map));
> GLPM(dl_rtld_map).l_real = &GLPM(dl_rtld_map);
> GLPM(dl_rtld_map).l_map_start = (ElfW(Addr)) &__ehdr_start;
> GLPM(dl_rtld_map).l_map_end = (ElfW(Addr)) _end;
> - /* Copy the TLS related data if necessary. */
> #ifndef DONT_USE_BOOTSTRAP_MAP
> # if NO_TLS_OFFSET != 0
> GLPM(dl_rtld_map).l_rw->l_tls_offset = NO_TLS_OFFSET;
> @@ -537,43 +534,59 @@ _dl_start (void *arg)
> rtld_timer_start (&info.start_time);
> #endif
>
> - /* Partly clean the `bootstrap_map' structure up. Don't use
> - `memset' since it might not be built in or inlined and we cannot
> - make function calls at this point. Use '__builtin_memset' if we
> - know it is available. We do not have to clear the memory if we
> - do not have to use the temporary bootstrap_map. Global variables
> - are initialized to zero by default. */
> -#ifndef DONT_USE_BOOTSTRAP_MAP
> -# ifdef HAVE_BUILTIN_MEMSET
> - __builtin_memset (bootstrap_map.l_info, '\0', sizeof (bootstrap_map.l_info));
> -# else
> - for (size_t cnt = 0;
> - cnt < sizeof (bootstrap_map.l_info) / sizeof (bootstrap_map.l_info[0]);
> - ++cnt)
> - bootstrap_map.l_info[cnt] = 0;
> -# endif
> + struct rtld_protmem *protmem = _dl_protmem_bootstrap ();
> + bool protmem_failed = protmem == NULL;
> + if (protmem_failed)
> + {
> + /* Allocate some space for a stub protected memory area on the
> + stack, to get to the point when we can report the error. */
> + protmem = alloca (sizeof (*protmem));
> +
> + /* Partly clean the `bootstrap_map' structure up. Don't use
> + `memset' since it might not be built in or inlined and we
> + cannot make function calls at this point. Use
> + '__builtin_memset' if we know it is available. */
> +#ifdef HAVE_BUILTIN_MEMSET
> + __builtin_memset (protmem->_dl_rtld_map.l_info,
> + '\0', sizeof (protmem->_dl_rtld_map.l_info));
> +#else
> + for (size_t i = 0; i < array_length (protmem->_dl_rtld_map.l_info); ++i)
> + protmem->_dl_rtld_map.l_info[i] = NULL;
> #endif
> + }
>
> /* Figure out the run-time load address of the dynamic linker itself. */
> - bootstrap_map.l_addr = elf_machine_load_address ();
> + protmem->_dl_rtld_map.l_addr = elf_machine_load_address ();
>
> /* Read our own dynamic section and fill in the info array. */
> - bootstrap_map.l_ld = (void *) bootstrap_map.l_addr + elf_machine_dynamic ();
> - bootstrap_map.l_ld_readonly = DL_RO_DYN_SECTION;
> - elf_get_dynamic_info (&bootstrap_map, true, false);
> + protmem->_dl_rtld_map.l_ld = ((void *) protmem->_dl_rtld_map.l_addr
> + + elf_machine_dynamic ());
> + protmem->_dl_rtld_map.l_ld_readonly = DL_RO_DYN_SECTION;
> + elf_get_dynamic_info (&protmem->_dl_rtld_map, true, false);
>
> #ifdef ELF_MACHINE_BEFORE_RTLD_RELOC
> - ELF_MACHINE_BEFORE_RTLD_RELOC (&bootstrap_map, bootstrap_map.l_info);
> + ELF_MACHINE_BEFORE_RTLD_RELOC (&protmem->_dl_rtld_map,
> + protmem->_dl_rtld_map.l_info);
> #endif
>
> - if (bootstrap_map.l_addr)
> + if (protmem->_dl_rtld_map.l_addr)
> {
> /* Relocate ourselves so we can do normal function calls and
> data access using the global offset table. */
>
> - ELF_DYNAMIC_RELOCATE (&bootstrap_map, NULL, 0, 0, 0);
> + ELF_DYNAMIC_RELOCATE (&protmem->_dl_rtld_map, NULL, 0, 0, 0);
> }
> - bootstrap_map.l_relocated = 1;
> + protmem->_dl_rtld_map.l_relocated = 1;
> +
> + /* Communicate the original mmap failure to _dl_start_final. */
> + if (protmem_failed)
> + protmem = NULL;
> +
> +#ifdef DONT_USE_BOOTSTRAP_MAP
> + GLRO (dl_protmem) = protmem;
> +#else
> + info.protmem = protmem;
> +#endif
>
> /* Please note that we don't allow profiling of this object and
> therefore need not test whether we have to allocate the array
> @@ -1024,7 +1037,7 @@ ERROR: audit interface '%s' requires version %d (maximum supported version %d);
> else
> *last_audit = (*last_audit)->next = &newp->ifaces;
>
> - /* The dynamic linker link map is statically allocated, so the
> + /* The dynamic linker link map is allocated separately, so the
> cookie in _dl_new_object has not happened. */
> link_map_audit_state (&GLPM(dl_rtld_map), GLRO (dl_naudit))->cookie
> = (intptr_t) &GLPM(dl_rtld_map);
> diff --git a/elf/tst-rtld-nomem.c b/elf/tst-rtld-nomem.c
> new file mode 100644
> index 0000000000..b8caf5d8fe
> --- /dev/null
> +++ b/elf/tst-rtld-nomem.c
> @@ -0,0 +1,177 @@
> +/* Test that out-of-memory during early ld.so startup reports an error.
> + Copyright (C) 2025 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 invokes execve with increasing RLIMIT_AS limits, to
> + trigger the early _dl_protmem_bootstrap memory allocation failure
> + and check that a proper error is reported for it. */
> +
> +#include <errno.h>
> +#include <stdbool.h>
> +#include <stddef.h>
> +#include <stdio.h>
> +#include <string.h>
> +#include <support/check.h>
> +#include <support/support.h>
> +#include <support/xunistd.h>
> +#include <sys/resource.h>
> +#include <sys/wait.h>
> +#include <unistd.h>
> +
> +static int
> +do_test (void)
> +{
> + long int page_size = sysconf (_SC_PAGE_SIZE);
> + TEST_VERIFY (page_size > 0);
> +
> + struct rlimit rlim;
> + TEST_COMPARE (getrlimit (RLIMIT_AS, &rlim), 0);
> +
> + /* Reduced once we encounter success. */
> + int kb_limit = 2048;
> +
> + /* Exit status in case of test error. */
> + enum { unexpected_error = 17 };
> +
> + /* Used to verify that at least one execve crash is encountered.
> + This is how execve reports late memory allocation failures due
> + to rlimit. */
> + bool crash_seen = false;
> +
> + /* Set to true if the early out-of-memory error message is
> + encountered. */
> + bool oom_error_seen = false;
> +
> + /* Set to true once success (the usage message) is encountered.
> + This is expected to happen only after oom_error_seen turns true,
> + otherwise the rlimit does not work. */
> + bool success_seen = false;
> +
> + /* Try increasing rlimits. The kernel rounds down to page sizes, so
> + try only page size increments. */
> + for (int kb = 128; kb <= kb_limit; kb += page_size / 1024)
> + {
> + printf ("info: trying %d KiB\n", kb);
> +
> + int pipe_stdout[2];
> + xpipe (pipe_stdout);
> + int pipe_stderr[2];
> + xpipe (pipe_stderr);
> +
> + pid_t pid = xfork ();
> + if (pid == 0)
> + {
> + /* Restrict address space for the ld.so invocation. */
> + rlim.rlim_cur = kb * 1024;
> + int ret = setrlimit (RLIMIT_AS, &rlim);
> + TEST_COMPARE (ret, 0);
> + if (ret != 0)
> + _exit (unexpected_error);
> +
> + /* Redirect output for capture. */
> + TEST_COMPARE (dup2 (pipe_stdout[1], STDOUT_FILENO),
> + STDOUT_FILENO);
> + TEST_COMPARE (dup2 (pipe_stderr[1], STDERR_FILENO),
> + STDERR_FILENO);
> +
> + /* Try to invoke ld.so with the resource limit in place. */
> + char ldso[] = "ld.so";
> + char *const argv[] = { ldso, NULL };
> + execve (support_objdir_elf_ldso, argv, &argv[1]);
> + TEST_COMPARE (errno, ENOMEM);
> + _exit (unexpected_error);
> + }
> +
> + int status;
> + xwaitpid (pid, &status, 0);
> +
> + xclose (pipe_stdout[1]);
> + xclose (pipe_stderr[1]);
> +
> + /* No output on stdout. */
> + char actual[1024];
> + ssize_t count = read (pipe_stdout[0], actual, sizeof (actual));
> + if (count < 0)
> + FAIL_EXIT1 ("read stdout: %m");
> + TEST_COMPARE_BLOB ("", 0, actual, count);
> +
> + /* Read the standard error output. */
> + count = read (pipe_stderr[0], actual, sizeof (actual));
> + if (count < 0)
> + FAIL_EXIT1 ("read stderr: %m");
> +
> + if (WIFEXITED (status) && WEXITSTATUS (status) == 1)
> + {
> + TEST_VERIFY (oom_error_seen);
> + static const char expected[] = "\
> +ld.so: missing program name\n\
> +Try 'ld.so --help' for more information.\n\
> +";
> + TEST_COMPARE_BLOB (expected, strlen (expected), actual, count);
> + if (!success_seen)
> + {
> + puts ("info: first success");
> + /* Four more tries with increasing rlimit, to catch
> + potential secondary crashes. */
> + kb_limit = kb + page_size / 1024 * 4;
> + }
> + success_seen = true;
> + continue;
> + }
> + if (WIFEXITED (status) && WEXITSTATUS (status) == 127)
> + {
> + TEST_VERIFY (crash_seen);
> + TEST_VERIFY (!success_seen);
> + static const char expected[] =
> + "Fatal glibc error: Cannot allocate link map\n";
> + TEST_COMPARE_BLOB (expected, strlen (expected), actual, count);
> + if (!oom_error_seen)
> + puts ("info: first memory allocation error");
> + oom_error_seen = true;
> + continue;
> + }
> +
> + TEST_VERIFY (!success_seen);
> + TEST_VERIFY (!oom_error_seen);
> +
> + if (WIFEXITED (status))
> + {
> + /* Unexpected regular exit status. */
> + TEST_COMPARE (WIFEXITED (status), 1);
> + TEST_COMPARE_BLOB ("", 0, actual, count);
> + }
> + else if (WIFSIGNALED (status) && WTERMSIG (status) == SIGSEGV)
> + {
> + /* Very early out of memory. No output expected. */
> + TEST_COMPARE_BLOB ("", 0, actual, count);
> + if (!crash_seen)
> + puts ("info: first expected crash observed");
> + crash_seen = true;
> + }
> + else
> + {
> + /* Unexpected status. */
> + printf ("error: unexpected exit status %d\n", status);
> + support_record_failure ();
> + TEST_COMPARE_BLOB ("", 0, actual, count);
> + }
> + }
> +
> + return 0;
> +}
> +
> +#include <support/test-driver.c>
> diff --git a/sysdeps/generic/dl-early_mmap.h b/sysdeps/generic/dl-early_mmap.h
> new file mode 100644
> index 0000000000..75eb8eb30c
> --- /dev/null
> +++ b/sysdeps/generic/dl-early_mmap.h
> @@ -0,0 +1,35 @@
> +/* Early anonymous mmap for ld.so, before self-relocation. Generic version.
> + Copyright (C) 2025 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/>. */
> +
> +#ifndef DL_EARLY_MMAP_H
> +#define DL_EARLY_MMAP_H
> +
> +/* The generic version assumes that regular mmap works. It returns
> + NULL on failure. */
> +static inline void *
> +_dl_early_mmap (size_t size)
> +{
> + void *ret = __mmap (NULL, size, PROT_READ | PROT_WRITE,
> + MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
> + if (ret == MAP_FAILED)
> + return NULL;
> + else
> + return ret;
> +}
> +
> +#endif /* DL_EARLY_MMAP_H */
> diff --git a/sysdeps/generic/ldsodefs.h b/sysdeps/generic/ldsodefs.h
> index ac71668f29..0ff0650cb1 100644
> --- a/sysdeps/generic/ldsodefs.h
> +++ b/sysdeps/generic/ldsodefs.h
> @@ -515,12 +515,11 @@ struct rtld_protmem
> /* Structure describing the dynamic linker itself. */
> struct link_map _dl_rtld_map;
> };
> -extern struct rtld_protmem _rtld_protmem attribute_hidden;
> #endif /* SHARED */
>
> /* GLPM(FIELD) denotes the FIELD in the protected memory area. */
> #ifdef SHARED
> -# define GLPM(name) _rtld_protmem._##name
> +# define GLPM(name) GLRO (dl_protmem)->_##name
> #else
> # define GLPM(name) _##name
> #endif
> @@ -660,6 +659,9 @@ struct rtld_global_ro
> EXTERN enum dso_sort_algorithm _dl_dso_sort_algo;
>
> #ifdef SHARED
> + /* Pointer to the protected memory area. */
> + EXTERN struct rtld_protmem *_dl_protmem;
> +
> /* We add a function table to _rtld_global which is then used to
> call the function instead of going through the PLT. The result
> is that we can avoid exporting the functions and we do not jump
> diff --git a/sysdeps/mips/Makefile b/sysdeps/mips/Makefile
> index d189973aa0..2ec9bf2a6c 100644
> --- a/sysdeps/mips/Makefile
> +++ b/sysdeps/mips/Makefile
> @@ -32,6 +32,12 @@ test-xfail-tst-audit24d = yes
> test-xfail-tst-audit25a = yes
> test-xfail-tst-audit25b = yes
>
> +# _dl_start performs a system call before self-relocation, to allocate
> +# the link map for ld.so itself. This involves a direct function
> +# call. Build rtld.c in MIPS32 mode, so that this function call does
> +# not require a run-time relocation.
> +CFLAGS-rtld.c += -mno-mips16
> +
> ifneq ($(o32-fpabi),)
> tests += tst-abi-interlink
>
> diff --git a/sysdeps/unix/sysv/linux/dl-early_allocate.c b/sysdeps/unix/sysv/linux/dl-early_allocate.c
> index 257519b789..ca7121d52e 100644
> --- a/sysdeps/unix/sysv/linux/dl-early_allocate.c
> +++ b/sysdeps/unix/sysv/linux/dl-early_allocate.c
> @@ -29,7 +29,7 @@
> #include <unistd.h>
>
> #include <brk_call.h>
> -#include <mmap_call.h>
> +#include <dl-early_mmap.h>
>
> /* Defined in brk.c. */
> extern void *__curbrk;
> @@ -63,20 +63,7 @@ _dl_early_allocate (size_t size)
> unfortunate ASLR layout decisions and kernel bugs, particularly
> for static PIE. */
> if (result == NULL)
> - {
> - long int ret;
> - int prot = PROT_READ | PROT_WRITE;
> - int flags = MAP_PRIVATE | MAP_ANONYMOUS;
> -#ifdef __NR_mmap2
> - ret = MMAP_CALL_INTERNAL (mmap2, 0, size, prot, flags, -1, 0);
> -#else
> - ret = MMAP_CALL_INTERNAL (mmap, 0, size, prot, flags, -1, 0);
> -#endif
> - if (INTERNAL_SYSCALL_ERROR_P (ret))
> - result = NULL;
> - else
> - result = (void *) ret;
> - }
> + result = _dl_early_mmap (size);
>
> return result;
> }
> diff --git a/sysdeps/unix/sysv/linux/dl-early_mmap.h b/sysdeps/unix/sysv/linux/dl-early_mmap.h
> new file mode 100644
> index 0000000000..1d83daa6a6
> --- /dev/null
> +++ b/sysdeps/unix/sysv/linux/dl-early_mmap.h
> @@ -0,0 +1,41 @@
> +/* Early anonymous mmap for ld.so, before self-relocation. Linux version.
> + Copyright (C) 2022-2023 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/>. */
> +
> +#ifndef DL_EARLY_MMAP_H
> +#define DL_EARLY_MMAP_H
> +
> +#include <mmap_call.h>
> +
> +static inline __attribute__ ((always_inline)) void *
> +_dl_early_mmap (size_t size)
> +{
> + long int ret;
> + int prot = PROT_READ | PROT_WRITE;
> + int flags = MAP_PRIVATE | MAP_ANONYMOUS;
> +#ifdef __NR_mmap2
> + ret = MMAP_CALL_INTERNAL (mmap2, 0, size, prot, flags, -1, 0);
> +#else
> + ret = MMAP_CALL_INTERNAL (mmap, 0, size, prot, flags, -1, 0);
> +#endif
> + if (INTERNAL_SYSCALL_ERROR_P (ret))
> + return NULL;
> + else
> + return (void *) ret;
> +}
> +
> +#endif /* DL_EARLY_MMAP_H */
On 02/02/25 18:13, Florian Weimer wrote:
> A subsequent change will place link maps into memory which is
> read-only most of the time. This means that the link map for
> ld.so itself (GLPM (dl_rtld_map)) needs to be put there as well,
> which requires allocating it dynamically.
Is an extra mmap on each program invocation, with all the extra error
handling complexity (which it requires to move the handling later in
self-relocation), really worth here?
Maybe we can either define a supported maximum page (similar to your
work on "Teach glibc about possible page sizes and handle gaps in ld.so"
patchset), or get if from ld thorough configure; and use it to define
both alignment and size for the rtld_protmem. The ld can either later
move it to relro or apply read-only protection itself.
> ---
> elf/Makefile | 1 +
> elf/dl-protmem_bootstrap.h | 29 ++++
> elf/rtld.c | 87 ++++++----
> elf/tst-rtld-nomem.c | 177 ++++++++++++++++++++
> sysdeps/generic/dl-early_mmap.h | 35 ++++
> sysdeps/generic/ldsodefs.h | 6 +-
> sysdeps/mips/Makefile | 6 +
> sysdeps/unix/sysv/linux/dl-early_allocate.c | 17 +-
> sysdeps/unix/sysv/linux/dl-early_mmap.h | 41 +++++
> 9 files changed, 345 insertions(+), 54 deletions(-)
> create mode 100644 elf/dl-protmem_bootstrap.h
> create mode 100644 elf/tst-rtld-nomem.c
> create mode 100644 sysdeps/generic/dl-early_mmap.h
> create mode 100644 sysdeps/unix/sysv/linux/dl-early_mmap.h
>
> diff --git a/elf/Makefile b/elf/Makefile
> index 5c833871d0..1d93993241 100644
> --- a/elf/Makefile
> +++ b/elf/Makefile
> @@ -463,6 +463,7 @@ tests += \
> tst-rtld-no-malloc \
> tst-rtld-no-malloc-audit \
> tst-rtld-no-malloc-preload \
> + tst-rtld-nomem \
> tst-rtld-run-static \
> tst-single_threaded \
> tst-single_threaded-pthread \
> diff --git a/elf/dl-protmem_bootstrap.h b/elf/dl-protmem_bootstrap.h
> new file mode 100644
> index 0000000000..a2fc267a2d
> --- /dev/null
> +++ b/elf/dl-protmem_bootstrap.h
> @@ -0,0 +1,29 @@
> +/* Bootstrap allocation for the protected memory area.
> + Copyright (C) 2025 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/>. */
> +
> +#include <dl-early_mmap.h>
> +
> +/* Return a pointer to the protected memory area, or NULL if
> + allocation fails. This function is called before self-relocation,
> + and the system call needs to be inlined for (most)
> + HIDDEN_VAR_NEEDS_DYNAMIC_RELOC targets. */
> +static inline __attribute__ ((always_inline)) struct rtld_protmem *
> +_dl_protmem_bootstrap (void)
> +{
> + return _dl_early_mmap (sizeof (struct rtld_protmem));
> +}
> diff --git a/elf/rtld.c b/elf/rtld.c
> index 0bc7d9dbcd..de9e87cd0b 100644
> --- a/elf/rtld.c
> +++ b/elf/rtld.c
> @@ -53,6 +53,7 @@
> #include <dl-find_object.h>
> #include <dl-audit-check.h>
> #include <dl-call_tls_init_tp.h>
> +#include <dl-protmem_bootstrap.h>
>
> #include <assert.h>
>
> @@ -345,8 +346,6 @@ struct rtld_global _rtld_global =
> extern struct rtld_global _rtld_local
> __attribute__ ((alias ("_rtld_global"), visibility ("hidden")));
>
> -struct rtld_protmem _rtld_protmem;
> -
> /* This variable is similar to _rtld_local, but all values are
> read-only after relocation. */
> struct rtld_global_ro _rtld_global_ro attribute_relro =
> @@ -421,6 +420,7 @@ static ElfW(Addr) _dl_start_final (void *arg);
> struct dl_start_final_info
> {
> struct link_map l;
> + struct rtld_protmem *protmem;
> RTLD_TIMING_VAR (start_time);
> };
> static ElfW(Addr) _dl_start_final (void *arg,
> @@ -455,6 +455,14 @@ _dl_start_final (void *arg, struct dl_start_final_info *info)
> {
> ElfW(Addr) start_addr;
>
> +#ifndef DONT_USE_BOOTSTRAP_MAP
> + GLRO (dl_protmem) = info->protmem;
> +#endif
> +
> + /* Delayed error reporting after relocation processing. */
> + if (GLRO (dl_protmem) == NULL)
> + _dl_fatal_printf ("Fatal glibc error: Cannot allocate link map\n");
> +
> __rtld_malloc_init_stubs ();
>
> /* Do not use an initializer for these members because it would
> @@ -478,21 +486,10 @@ _dl_start_final (void *arg, struct dl_start_final_info *info)
> RTLD_TIMING_SET (start_time, info->start_time);
> #endif
>
> - /* Transfer data about ourselves to the permanent link_map structure. */
> -#ifndef DONT_USE_BOOTSTRAP_MAP
> - GLPM(dl_rtld_map).l_addr = info->l.l_addr;
> - GLPM(dl_rtld_map).l_ld = info->l.l_ld;
> - GLPM(dl_rtld_map).l_ld_readonly = info->l.l_ld_readonly;
> - memcpy (GLPM(dl_rtld_map).l_info, info->l.l_info,
> - sizeof GLPM(dl_rtld_map).l_info);
> - GLPM(dl_rtld_map).l_mach = info->l.l_mach;
> - GLPM(dl_rtld_map).l_relocated = 1;
> -#endif
> _dl_setup_hash (&GLPM(dl_rtld_map));
> GLPM(dl_rtld_map).l_real = &GLPM(dl_rtld_map);
> GLPM(dl_rtld_map).l_map_start = (ElfW(Addr)) &__ehdr_start;
> GLPM(dl_rtld_map).l_map_end = (ElfW(Addr)) _end;
> - /* Copy the TLS related data if necessary. */
> #ifndef DONT_USE_BOOTSTRAP_MAP
> # if NO_TLS_OFFSET != 0
> GLPM(dl_rtld_map).l_rw->l_tls_offset = NO_TLS_OFFSET;
> @@ -537,43 +534,59 @@ _dl_start (void *arg)
> rtld_timer_start (&info.start_time);
> #endif
>
> - /* Partly clean the `bootstrap_map' structure up. Don't use
> - `memset' since it might not be built in or inlined and we cannot
> - make function calls at this point. Use '__builtin_memset' if we
> - know it is available. We do not have to clear the memory if we
> - do not have to use the temporary bootstrap_map. Global variables
> - are initialized to zero by default. */
> -#ifndef DONT_USE_BOOTSTRAP_MAP
> -# ifdef HAVE_BUILTIN_MEMSET
> - __builtin_memset (bootstrap_map.l_info, '\0', sizeof (bootstrap_map.l_info));
> -# else
> - for (size_t cnt = 0;
> - cnt < sizeof (bootstrap_map.l_info) / sizeof (bootstrap_map.l_info[0]);
> - ++cnt)
> - bootstrap_map.l_info[cnt] = 0;
> -# endif
> + struct rtld_protmem *protmem = _dl_protmem_bootstrap ();
> + bool protmem_failed = protmem == NULL;
> + if (protmem_failed)
> + {
> + /* Allocate some space for a stub protected memory area on the
> + stack, to get to the point when we can report the error. */
> + protmem = alloca (sizeof (*protmem));
> +
> + /* Partly clean the `bootstrap_map' structure up. Don't use
> + `memset' since it might not be built in or inlined and we
> + cannot make function calls at this point. Use
> + '__builtin_memset' if we know it is available. */
> +#ifdef HAVE_BUILTIN_MEMSET
> + __builtin_memset (protmem->_dl_rtld_map.l_info,
> + '\0', sizeof (protmem->_dl_rtld_map.l_info));
> +#else
> + for (size_t i = 0; i < array_length (protmem->_dl_rtld_map.l_info); ++i)
> + protmem->_dl_rtld_map.l_info[i] = NULL;
> #endif
> + }
>
> /* Figure out the run-time load address of the dynamic linker itself. */
> - bootstrap_map.l_addr = elf_machine_load_address ();
> + protmem->_dl_rtld_map.l_addr = elf_machine_load_address ();
>
> /* Read our own dynamic section and fill in the info array. */
> - bootstrap_map.l_ld = (void *) bootstrap_map.l_addr + elf_machine_dynamic ();
> - bootstrap_map.l_ld_readonly = DL_RO_DYN_SECTION;
> - elf_get_dynamic_info (&bootstrap_map, true, false);
> + protmem->_dl_rtld_map.l_ld = ((void *) protmem->_dl_rtld_map.l_addr
> + + elf_machine_dynamic ());
> + protmem->_dl_rtld_map.l_ld_readonly = DL_RO_DYN_SECTION;
> + elf_get_dynamic_info (&protmem->_dl_rtld_map, true, false);
>
> #ifdef ELF_MACHINE_BEFORE_RTLD_RELOC
> - ELF_MACHINE_BEFORE_RTLD_RELOC (&bootstrap_map, bootstrap_map.l_info);
> + ELF_MACHINE_BEFORE_RTLD_RELOC (&protmem->_dl_rtld_map,
> + protmem->_dl_rtld_map.l_info);
> #endif
>
> - if (bootstrap_map.l_addr)
> + if (protmem->_dl_rtld_map.l_addr)
> {
> /* Relocate ourselves so we can do normal function calls and
> data access using the global offset table. */
>
> - ELF_DYNAMIC_RELOCATE (&bootstrap_map, NULL, 0, 0, 0);
> + ELF_DYNAMIC_RELOCATE (&protmem->_dl_rtld_map, NULL, 0, 0, 0);
> }
> - bootstrap_map.l_relocated = 1;
> + protmem->_dl_rtld_map.l_relocated = 1;
> +
> + /* Communicate the original mmap failure to _dl_start_final. */
> + if (protmem_failed)
> + protmem = NULL;
> +
> +#ifdef DONT_USE_BOOTSTRAP_MAP
> + GLRO (dl_protmem) = protmem;
> +#else
> + info.protmem = protmem;
> +#endif
>
> /* Please note that we don't allow profiling of this object and
> therefore need not test whether we have to allocate the array
> @@ -1024,7 +1037,7 @@ ERROR: audit interface '%s' requires version %d (maximum supported version %d);
> else
> *last_audit = (*last_audit)->next = &newp->ifaces;
>
> - /* The dynamic linker link map is statically allocated, so the
> + /* The dynamic linker link map is allocated separately, so the
> cookie in _dl_new_object has not happened. */
> link_map_audit_state (&GLPM(dl_rtld_map), GLRO (dl_naudit))->cookie
> = (intptr_t) &GLPM(dl_rtld_map);
> diff --git a/elf/tst-rtld-nomem.c b/elf/tst-rtld-nomem.c
> new file mode 100644
> index 0000000000..b8caf5d8fe
> --- /dev/null
> +++ b/elf/tst-rtld-nomem.c
> @@ -0,0 +1,177 @@
> +/* Test that out-of-memory during early ld.so startup reports an error.
> + Copyright (C) 2025 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 invokes execve with increasing RLIMIT_AS limits, to
> + trigger the early _dl_protmem_bootstrap memory allocation failure
> + and check that a proper error is reported for it. */
> +
> +#include <errno.h>
> +#include <stdbool.h>
> +#include <stddef.h>
> +#include <stdio.h>
> +#include <string.h>
> +#include <support/check.h>
> +#include <support/support.h>
> +#include <support/xunistd.h>
> +#include <sys/resource.h>
> +#include <sys/wait.h>
> +#include <unistd.h>
> +
> +static int
> +do_test (void)
> +{
> + long int page_size = sysconf (_SC_PAGE_SIZE);
> + TEST_VERIFY (page_size > 0);
> +
> + struct rlimit rlim;
> + TEST_COMPARE (getrlimit (RLIMIT_AS, &rlim), 0);
> +
> + /* Reduced once we encounter success. */
> + int kb_limit = 2048;
> +
> + /* Exit status in case of test error. */
> + enum { unexpected_error = 17 };
> +
> + /* Used to verify that at least one execve crash is encountered.
> + This is how execve reports late memory allocation failures due
> + to rlimit. */
> + bool crash_seen = false;
> +
> + /* Set to true if the early out-of-memory error message is
> + encountered. */
> + bool oom_error_seen = false;
> +
> + /* Set to true once success (the usage message) is encountered.
> + This is expected to happen only after oom_error_seen turns true,
> + otherwise the rlimit does not work. */
> + bool success_seen = false;
> +
> + /* Try increasing rlimits. The kernel rounds down to page sizes, so
> + try only page size increments. */
> + for (int kb = 128; kb <= kb_limit; kb += page_size / 1024)
> + {
> + printf ("info: trying %d KiB\n", kb);
> +
> + int pipe_stdout[2];
> + xpipe (pipe_stdout);
> + int pipe_stderr[2];
> + xpipe (pipe_stderr);
> +
> + pid_t pid = xfork ();
> + if (pid == 0)
> + {
> + /* Restrict address space for the ld.so invocation. */
> + rlim.rlim_cur = kb * 1024;
> + int ret = setrlimit (RLIMIT_AS, &rlim);
> + TEST_COMPARE (ret, 0);
> + if (ret != 0)
> + _exit (unexpected_error);
> +
> + /* Redirect output for capture. */
> + TEST_COMPARE (dup2 (pipe_stdout[1], STDOUT_FILENO),
> + STDOUT_FILENO);
> + TEST_COMPARE (dup2 (pipe_stderr[1], STDERR_FILENO),
> + STDERR_FILENO);
> +
> + /* Try to invoke ld.so with the resource limit in place. */
> + char ldso[] = "ld.so";
> + char *const argv[] = { ldso, NULL };
> + execve (support_objdir_elf_ldso, argv, &argv[1]);
> + TEST_COMPARE (errno, ENOMEM);
> + _exit (unexpected_error);
> + }
> +
> + int status;
> + xwaitpid (pid, &status, 0);
> +
> + xclose (pipe_stdout[1]);
> + xclose (pipe_stderr[1]);
> +
> + /* No output on stdout. */
> + char actual[1024];
> + ssize_t count = read (pipe_stdout[0], actual, sizeof (actual));
> + if (count < 0)
> + FAIL_EXIT1 ("read stdout: %m");
> + TEST_COMPARE_BLOB ("", 0, actual, count);
> +
> + /* Read the standard error output. */
> + count = read (pipe_stderr[0], actual, sizeof (actual));
> + if (count < 0)
> + FAIL_EXIT1 ("read stderr: %m");
> +
> + if (WIFEXITED (status) && WEXITSTATUS (status) == 1)
> + {
> + TEST_VERIFY (oom_error_seen);
> + static const char expected[] = "\
> +ld.so: missing program name\n\
> +Try 'ld.so --help' for more information.\n\
> +";
> + TEST_COMPARE_BLOB (expected, strlen (expected), actual, count);
> + if (!success_seen)
> + {
> + puts ("info: first success");
> + /* Four more tries with increasing rlimit, to catch
> + potential secondary crashes. */
> + kb_limit = kb + page_size / 1024 * 4;
> + }
> + success_seen = true;
> + continue;
> + }
> + if (WIFEXITED (status) && WEXITSTATUS (status) == 127)
> + {
> + TEST_VERIFY (crash_seen);
> + TEST_VERIFY (!success_seen);
> + static const char expected[] =
> + "Fatal glibc error: Cannot allocate link map\n";
> + TEST_COMPARE_BLOB (expected, strlen (expected), actual, count);
> + if (!oom_error_seen)
> + puts ("info: first memory allocation error");
> + oom_error_seen = true;
> + continue;
> + }
> +
> + TEST_VERIFY (!success_seen);
> + TEST_VERIFY (!oom_error_seen);
> +
> + if (WIFEXITED (status))
> + {
> + /* Unexpected regular exit status. */
> + TEST_COMPARE (WIFEXITED (status), 1);
> + TEST_COMPARE_BLOB ("", 0, actual, count);
> + }
> + else if (WIFSIGNALED (status) && WTERMSIG (status) == SIGSEGV)
> + {
> + /* Very early out of memory. No output expected. */
> + TEST_COMPARE_BLOB ("", 0, actual, count);
> + if (!crash_seen)
> + puts ("info: first expected crash observed");
> + crash_seen = true;
> + }
> + else
> + {
> + /* Unexpected status. */
> + printf ("error: unexpected exit status %d\n", status);
> + support_record_failure ();
> + TEST_COMPARE_BLOB ("", 0, actual, count);
> + }
> + }
> +
> + return 0;
> +}
> +
> +#include <support/test-driver.c>
> diff --git a/sysdeps/generic/dl-early_mmap.h b/sysdeps/generic/dl-early_mmap.h
> new file mode 100644
> index 0000000000..75eb8eb30c
> --- /dev/null
> +++ b/sysdeps/generic/dl-early_mmap.h
> @@ -0,0 +1,35 @@
> +/* Early anonymous mmap for ld.so, before self-relocation. Generic version.
> + Copyright (C) 2025 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/>. */
> +
> +#ifndef DL_EARLY_MMAP_H
> +#define DL_EARLY_MMAP_H
> +
> +/* The generic version assumes that regular mmap works. It returns
> + NULL on failure. */
> +static inline void *
> +_dl_early_mmap (size_t size)
> +{
> + void *ret = __mmap (NULL, size, PROT_READ | PROT_WRITE,
> + MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
> + if (ret == MAP_FAILED)
> + return NULL;
> + else
> + return ret;
> +}
> +
> +#endif /* DL_EARLY_MMAP_H */
> diff --git a/sysdeps/generic/ldsodefs.h b/sysdeps/generic/ldsodefs.h
> index ac71668f29..0ff0650cb1 100644
> --- a/sysdeps/generic/ldsodefs.h
> +++ b/sysdeps/generic/ldsodefs.h
> @@ -515,12 +515,11 @@ struct rtld_protmem
> /* Structure describing the dynamic linker itself. */
> struct link_map _dl_rtld_map;
> };
> -extern struct rtld_protmem _rtld_protmem attribute_hidden;
> #endif /* SHARED */
>
> /* GLPM(FIELD) denotes the FIELD in the protected memory area. */
> #ifdef SHARED
> -# define GLPM(name) _rtld_protmem._##name
> +# define GLPM(name) GLRO (dl_protmem)->_##name
> #else
> # define GLPM(name) _##name
> #endif
> @@ -660,6 +659,9 @@ struct rtld_global_ro
> EXTERN enum dso_sort_algorithm _dl_dso_sort_algo;
>
> #ifdef SHARED
> + /* Pointer to the protected memory area. */
> + EXTERN struct rtld_protmem *_dl_protmem;
> +
> /* We add a function table to _rtld_global which is then used to
> call the function instead of going through the PLT. The result
> is that we can avoid exporting the functions and we do not jump
> diff --git a/sysdeps/mips/Makefile b/sysdeps/mips/Makefile
> index d189973aa0..2ec9bf2a6c 100644
> --- a/sysdeps/mips/Makefile
> +++ b/sysdeps/mips/Makefile
> @@ -32,6 +32,12 @@ test-xfail-tst-audit24d = yes
> test-xfail-tst-audit25a = yes
> test-xfail-tst-audit25b = yes
>
> +# _dl_start performs a system call before self-relocation, to allocate
> +# the link map for ld.so itself. This involves a direct function
> +# call. Build rtld.c in MIPS32 mode, so that this function call does
> +# not require a run-time relocation.
> +CFLAGS-rtld.c += -mno-mips16
> +
> ifneq ($(o32-fpabi),)
> tests += tst-abi-interlink
>
> diff --git a/sysdeps/unix/sysv/linux/dl-early_allocate.c b/sysdeps/unix/sysv/linux/dl-early_allocate.c
> index 257519b789..ca7121d52e 100644
> --- a/sysdeps/unix/sysv/linux/dl-early_allocate.c
> +++ b/sysdeps/unix/sysv/linux/dl-early_allocate.c
> @@ -29,7 +29,7 @@
> #include <unistd.h>
>
> #include <brk_call.h>
> -#include <mmap_call.h>
> +#include <dl-early_mmap.h>
>
> /* Defined in brk.c. */
> extern void *__curbrk;
> @@ -63,20 +63,7 @@ _dl_early_allocate (size_t size)
> unfortunate ASLR layout decisions and kernel bugs, particularly
> for static PIE. */
> if (result == NULL)
> - {
> - long int ret;
> - int prot = PROT_READ | PROT_WRITE;
> - int flags = MAP_PRIVATE | MAP_ANONYMOUS;
> -#ifdef __NR_mmap2
> - ret = MMAP_CALL_INTERNAL (mmap2, 0, size, prot, flags, -1, 0);
> -#else
> - ret = MMAP_CALL_INTERNAL (mmap, 0, size, prot, flags, -1, 0);
> -#endif
> - if (INTERNAL_SYSCALL_ERROR_P (ret))
> - result = NULL;
> - else
> - result = (void *) ret;
> - }
> + result = _dl_early_mmap (size);
>
> return result;
> }
> diff --git a/sysdeps/unix/sysv/linux/dl-early_mmap.h b/sysdeps/unix/sysv/linux/dl-early_mmap.h
> new file mode 100644
> index 0000000000..1d83daa6a6
> --- /dev/null
> +++ b/sysdeps/unix/sysv/linux/dl-early_mmap.h
> @@ -0,0 +1,41 @@
> +/* Early anonymous mmap for ld.so, before self-relocation. Linux version.
> + Copyright (C) 2022-2023 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/>. */
> +
> +#ifndef DL_EARLY_MMAP_H
> +#define DL_EARLY_MMAP_H
> +
> +#include <mmap_call.h>
> +
> +static inline __attribute__ ((always_inline)) void *
> +_dl_early_mmap (size_t size)
> +{
> + long int ret;
> + int prot = PROT_READ | PROT_WRITE;
> + int flags = MAP_PRIVATE | MAP_ANONYMOUS;
> +#ifdef __NR_mmap2
> + ret = MMAP_CALL_INTERNAL (mmap2, 0, size, prot, flags, -1, 0);
> +#else
> + ret = MMAP_CALL_INTERNAL (mmap, 0, size, prot, flags, -1, 0);
> +#endif
> + if (INTERNAL_SYSCALL_ERROR_P (ret))
> + return NULL;
> + else
> + return (void *) ret;
> +}
> +
> +#endif /* DL_EARLY_MMAP_H */
@@ -463,6 +463,7 @@ tests += \
tst-rtld-no-malloc \
tst-rtld-no-malloc-audit \
tst-rtld-no-malloc-preload \
+ tst-rtld-nomem \
tst-rtld-run-static \
tst-single_threaded \
tst-single_threaded-pthread \
new file mode 100644
@@ -0,0 +1,29 @@
+/* Bootstrap allocation for the protected memory area.
+ Copyright (C) 2025 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/>. */
+
+#include <dl-early_mmap.h>
+
+/* Return a pointer to the protected memory area, or NULL if
+ allocation fails. This function is called before self-relocation,
+ and the system call needs to be inlined for (most)
+ HIDDEN_VAR_NEEDS_DYNAMIC_RELOC targets. */
+static inline __attribute__ ((always_inline)) struct rtld_protmem *
+_dl_protmem_bootstrap (void)
+{
+ return _dl_early_mmap (sizeof (struct rtld_protmem));
+}
@@ -53,6 +53,7 @@
#include <dl-find_object.h>
#include <dl-audit-check.h>
#include <dl-call_tls_init_tp.h>
+#include <dl-protmem_bootstrap.h>
#include <assert.h>
@@ -345,8 +346,6 @@ struct rtld_global _rtld_global =
extern struct rtld_global _rtld_local
__attribute__ ((alias ("_rtld_global"), visibility ("hidden")));
-struct rtld_protmem _rtld_protmem;
-
/* This variable is similar to _rtld_local, but all values are
read-only after relocation. */
struct rtld_global_ro _rtld_global_ro attribute_relro =
@@ -421,6 +420,7 @@ static ElfW(Addr) _dl_start_final (void *arg);
struct dl_start_final_info
{
struct link_map l;
+ struct rtld_protmem *protmem;
RTLD_TIMING_VAR (start_time);
};
static ElfW(Addr) _dl_start_final (void *arg,
@@ -455,6 +455,14 @@ _dl_start_final (void *arg, struct dl_start_final_info *info)
{
ElfW(Addr) start_addr;
+#ifndef DONT_USE_BOOTSTRAP_MAP
+ GLRO (dl_protmem) = info->protmem;
+#endif
+
+ /* Delayed error reporting after relocation processing. */
+ if (GLRO (dl_protmem) == NULL)
+ _dl_fatal_printf ("Fatal glibc error: Cannot allocate link map\n");
+
__rtld_malloc_init_stubs ();
/* Do not use an initializer for these members because it would
@@ -478,21 +486,10 @@ _dl_start_final (void *arg, struct dl_start_final_info *info)
RTLD_TIMING_SET (start_time, info->start_time);
#endif
- /* Transfer data about ourselves to the permanent link_map structure. */
-#ifndef DONT_USE_BOOTSTRAP_MAP
- GLPM(dl_rtld_map).l_addr = info->l.l_addr;
- GLPM(dl_rtld_map).l_ld = info->l.l_ld;
- GLPM(dl_rtld_map).l_ld_readonly = info->l.l_ld_readonly;
- memcpy (GLPM(dl_rtld_map).l_info, info->l.l_info,
- sizeof GLPM(dl_rtld_map).l_info);
- GLPM(dl_rtld_map).l_mach = info->l.l_mach;
- GLPM(dl_rtld_map).l_relocated = 1;
-#endif
_dl_setup_hash (&GLPM(dl_rtld_map));
GLPM(dl_rtld_map).l_real = &GLPM(dl_rtld_map);
GLPM(dl_rtld_map).l_map_start = (ElfW(Addr)) &__ehdr_start;
GLPM(dl_rtld_map).l_map_end = (ElfW(Addr)) _end;
- /* Copy the TLS related data if necessary. */
#ifndef DONT_USE_BOOTSTRAP_MAP
# if NO_TLS_OFFSET != 0
GLPM(dl_rtld_map).l_rw->l_tls_offset = NO_TLS_OFFSET;
@@ -537,43 +534,59 @@ _dl_start (void *arg)
rtld_timer_start (&info.start_time);
#endif
- /* Partly clean the `bootstrap_map' structure up. Don't use
- `memset' since it might not be built in or inlined and we cannot
- make function calls at this point. Use '__builtin_memset' if we
- know it is available. We do not have to clear the memory if we
- do not have to use the temporary bootstrap_map. Global variables
- are initialized to zero by default. */
-#ifndef DONT_USE_BOOTSTRAP_MAP
-# ifdef HAVE_BUILTIN_MEMSET
- __builtin_memset (bootstrap_map.l_info, '\0', sizeof (bootstrap_map.l_info));
-# else
- for (size_t cnt = 0;
- cnt < sizeof (bootstrap_map.l_info) / sizeof (bootstrap_map.l_info[0]);
- ++cnt)
- bootstrap_map.l_info[cnt] = 0;
-# endif
+ struct rtld_protmem *protmem = _dl_protmem_bootstrap ();
+ bool protmem_failed = protmem == NULL;
+ if (protmem_failed)
+ {
+ /* Allocate some space for a stub protected memory area on the
+ stack, to get to the point when we can report the error. */
+ protmem = alloca (sizeof (*protmem));
+
+ /* Partly clean the `bootstrap_map' structure up. Don't use
+ `memset' since it might not be built in or inlined and we
+ cannot make function calls at this point. Use
+ '__builtin_memset' if we know it is available. */
+#ifdef HAVE_BUILTIN_MEMSET
+ __builtin_memset (protmem->_dl_rtld_map.l_info,
+ '\0', sizeof (protmem->_dl_rtld_map.l_info));
+#else
+ for (size_t i = 0; i < array_length (protmem->_dl_rtld_map.l_info); ++i)
+ protmem->_dl_rtld_map.l_info[i] = NULL;
#endif
+ }
/* Figure out the run-time load address of the dynamic linker itself. */
- bootstrap_map.l_addr = elf_machine_load_address ();
+ protmem->_dl_rtld_map.l_addr = elf_machine_load_address ();
/* Read our own dynamic section and fill in the info array. */
- bootstrap_map.l_ld = (void *) bootstrap_map.l_addr + elf_machine_dynamic ();
- bootstrap_map.l_ld_readonly = DL_RO_DYN_SECTION;
- elf_get_dynamic_info (&bootstrap_map, true, false);
+ protmem->_dl_rtld_map.l_ld = ((void *) protmem->_dl_rtld_map.l_addr
+ + elf_machine_dynamic ());
+ protmem->_dl_rtld_map.l_ld_readonly = DL_RO_DYN_SECTION;
+ elf_get_dynamic_info (&protmem->_dl_rtld_map, true, false);
#ifdef ELF_MACHINE_BEFORE_RTLD_RELOC
- ELF_MACHINE_BEFORE_RTLD_RELOC (&bootstrap_map, bootstrap_map.l_info);
+ ELF_MACHINE_BEFORE_RTLD_RELOC (&protmem->_dl_rtld_map,
+ protmem->_dl_rtld_map.l_info);
#endif
- if (bootstrap_map.l_addr)
+ if (protmem->_dl_rtld_map.l_addr)
{
/* Relocate ourselves so we can do normal function calls and
data access using the global offset table. */
- ELF_DYNAMIC_RELOCATE (&bootstrap_map, NULL, 0, 0, 0);
+ ELF_DYNAMIC_RELOCATE (&protmem->_dl_rtld_map, NULL, 0, 0, 0);
}
- bootstrap_map.l_relocated = 1;
+ protmem->_dl_rtld_map.l_relocated = 1;
+
+ /* Communicate the original mmap failure to _dl_start_final. */
+ if (protmem_failed)
+ protmem = NULL;
+
+#ifdef DONT_USE_BOOTSTRAP_MAP
+ GLRO (dl_protmem) = protmem;
+#else
+ info.protmem = protmem;
+#endif
/* Please note that we don't allow profiling of this object and
therefore need not test whether we have to allocate the array
@@ -1024,7 +1037,7 @@ ERROR: audit interface '%s' requires version %d (maximum supported version %d);
else
*last_audit = (*last_audit)->next = &newp->ifaces;
- /* The dynamic linker link map is statically allocated, so the
+ /* The dynamic linker link map is allocated separately, so the
cookie in _dl_new_object has not happened. */
link_map_audit_state (&GLPM(dl_rtld_map), GLRO (dl_naudit))->cookie
= (intptr_t) &GLPM(dl_rtld_map);
new file mode 100644
@@ -0,0 +1,177 @@
+/* Test that out-of-memory during early ld.so startup reports an error.
+ Copyright (C) 2025 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 invokes execve with increasing RLIMIT_AS limits, to
+ trigger the early _dl_protmem_bootstrap memory allocation failure
+ and check that a proper error is reported for it. */
+
+#include <errno.h>
+#include <stdbool.h>
+#include <stddef.h>
+#include <stdio.h>
+#include <string.h>
+#include <support/check.h>
+#include <support/support.h>
+#include <support/xunistd.h>
+#include <sys/resource.h>
+#include <sys/wait.h>
+#include <unistd.h>
+
+static int
+do_test (void)
+{
+ long int page_size = sysconf (_SC_PAGE_SIZE);
+ TEST_VERIFY (page_size > 0);
+
+ struct rlimit rlim;
+ TEST_COMPARE (getrlimit (RLIMIT_AS, &rlim), 0);
+
+ /* Reduced once we encounter success. */
+ int kb_limit = 2048;
+
+ /* Exit status in case of test error. */
+ enum { unexpected_error = 17 };
+
+ /* Used to verify that at least one execve crash is encountered.
+ This is how execve reports late memory allocation failures due
+ to rlimit. */
+ bool crash_seen = false;
+
+ /* Set to true if the early out-of-memory error message is
+ encountered. */
+ bool oom_error_seen = false;
+
+ /* Set to true once success (the usage message) is encountered.
+ This is expected to happen only after oom_error_seen turns true,
+ otherwise the rlimit does not work. */
+ bool success_seen = false;
+
+ /* Try increasing rlimits. The kernel rounds down to page sizes, so
+ try only page size increments. */
+ for (int kb = 128; kb <= kb_limit; kb += page_size / 1024)
+ {
+ printf ("info: trying %d KiB\n", kb);
+
+ int pipe_stdout[2];
+ xpipe (pipe_stdout);
+ int pipe_stderr[2];
+ xpipe (pipe_stderr);
+
+ pid_t pid = xfork ();
+ if (pid == 0)
+ {
+ /* Restrict address space for the ld.so invocation. */
+ rlim.rlim_cur = kb * 1024;
+ int ret = setrlimit (RLIMIT_AS, &rlim);
+ TEST_COMPARE (ret, 0);
+ if (ret != 0)
+ _exit (unexpected_error);
+
+ /* Redirect output for capture. */
+ TEST_COMPARE (dup2 (pipe_stdout[1], STDOUT_FILENO),
+ STDOUT_FILENO);
+ TEST_COMPARE (dup2 (pipe_stderr[1], STDERR_FILENO),
+ STDERR_FILENO);
+
+ /* Try to invoke ld.so with the resource limit in place. */
+ char ldso[] = "ld.so";
+ char *const argv[] = { ldso, NULL };
+ execve (support_objdir_elf_ldso, argv, &argv[1]);
+ TEST_COMPARE (errno, ENOMEM);
+ _exit (unexpected_error);
+ }
+
+ int status;
+ xwaitpid (pid, &status, 0);
+
+ xclose (pipe_stdout[1]);
+ xclose (pipe_stderr[1]);
+
+ /* No output on stdout. */
+ char actual[1024];
+ ssize_t count = read (pipe_stdout[0], actual, sizeof (actual));
+ if (count < 0)
+ FAIL_EXIT1 ("read stdout: %m");
+ TEST_COMPARE_BLOB ("", 0, actual, count);
+
+ /* Read the standard error output. */
+ count = read (pipe_stderr[0], actual, sizeof (actual));
+ if (count < 0)
+ FAIL_EXIT1 ("read stderr: %m");
+
+ if (WIFEXITED (status) && WEXITSTATUS (status) == 1)
+ {
+ TEST_VERIFY (oom_error_seen);
+ static const char expected[] = "\
+ld.so: missing program name\n\
+Try 'ld.so --help' for more information.\n\
+";
+ TEST_COMPARE_BLOB (expected, strlen (expected), actual, count);
+ if (!success_seen)
+ {
+ puts ("info: first success");
+ /* Four more tries with increasing rlimit, to catch
+ potential secondary crashes. */
+ kb_limit = kb + page_size / 1024 * 4;
+ }
+ success_seen = true;
+ continue;
+ }
+ if (WIFEXITED (status) && WEXITSTATUS (status) == 127)
+ {
+ TEST_VERIFY (crash_seen);
+ TEST_VERIFY (!success_seen);
+ static const char expected[] =
+ "Fatal glibc error: Cannot allocate link map\n";
+ TEST_COMPARE_BLOB (expected, strlen (expected), actual, count);
+ if (!oom_error_seen)
+ puts ("info: first memory allocation error");
+ oom_error_seen = true;
+ continue;
+ }
+
+ TEST_VERIFY (!success_seen);
+ TEST_VERIFY (!oom_error_seen);
+
+ if (WIFEXITED (status))
+ {
+ /* Unexpected regular exit status. */
+ TEST_COMPARE (WIFEXITED (status), 1);
+ TEST_COMPARE_BLOB ("", 0, actual, count);
+ }
+ else if (WIFSIGNALED (status) && WTERMSIG (status) == SIGSEGV)
+ {
+ /* Very early out of memory. No output expected. */
+ TEST_COMPARE_BLOB ("", 0, actual, count);
+ if (!crash_seen)
+ puts ("info: first expected crash observed");
+ crash_seen = true;
+ }
+ else
+ {
+ /* Unexpected status. */
+ printf ("error: unexpected exit status %d\n", status);
+ support_record_failure ();
+ TEST_COMPARE_BLOB ("", 0, actual, count);
+ }
+ }
+
+ return 0;
+}
+
+#include <support/test-driver.c>
new file mode 100644
@@ -0,0 +1,35 @@
+/* Early anonymous mmap for ld.so, before self-relocation. Generic version.
+ Copyright (C) 2025 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/>. */
+
+#ifndef DL_EARLY_MMAP_H
+#define DL_EARLY_MMAP_H
+
+/* The generic version assumes that regular mmap works. It returns
+ NULL on failure. */
+static inline void *
+_dl_early_mmap (size_t size)
+{
+ void *ret = __mmap (NULL, size, PROT_READ | PROT_WRITE,
+ MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
+ if (ret == MAP_FAILED)
+ return NULL;
+ else
+ return ret;
+}
+
+#endif /* DL_EARLY_MMAP_H */
@@ -515,12 +515,11 @@ struct rtld_protmem
/* Structure describing the dynamic linker itself. */
struct link_map _dl_rtld_map;
};
-extern struct rtld_protmem _rtld_protmem attribute_hidden;
#endif /* SHARED */
/* GLPM(FIELD) denotes the FIELD in the protected memory area. */
#ifdef SHARED
-# define GLPM(name) _rtld_protmem._##name
+# define GLPM(name) GLRO (dl_protmem)->_##name
#else
# define GLPM(name) _##name
#endif
@@ -660,6 +659,9 @@ struct rtld_global_ro
EXTERN enum dso_sort_algorithm _dl_dso_sort_algo;
#ifdef SHARED
+ /* Pointer to the protected memory area. */
+ EXTERN struct rtld_protmem *_dl_protmem;
+
/* We add a function table to _rtld_global which is then used to
call the function instead of going through the PLT. The result
is that we can avoid exporting the functions and we do not jump
@@ -32,6 +32,12 @@ test-xfail-tst-audit24d = yes
test-xfail-tst-audit25a = yes
test-xfail-tst-audit25b = yes
+# _dl_start performs a system call before self-relocation, to allocate
+# the link map for ld.so itself. This involves a direct function
+# call. Build rtld.c in MIPS32 mode, so that this function call does
+# not require a run-time relocation.
+CFLAGS-rtld.c += -mno-mips16
+
ifneq ($(o32-fpabi),)
tests += tst-abi-interlink
@@ -29,7 +29,7 @@
#include <unistd.h>
#include <brk_call.h>
-#include <mmap_call.h>
+#include <dl-early_mmap.h>
/* Defined in brk.c. */
extern void *__curbrk;
@@ -63,20 +63,7 @@ _dl_early_allocate (size_t size)
unfortunate ASLR layout decisions and kernel bugs, particularly
for static PIE. */
if (result == NULL)
- {
- long int ret;
- int prot = PROT_READ | PROT_WRITE;
- int flags = MAP_PRIVATE | MAP_ANONYMOUS;
-#ifdef __NR_mmap2
- ret = MMAP_CALL_INTERNAL (mmap2, 0, size, prot, flags, -1, 0);
-#else
- ret = MMAP_CALL_INTERNAL (mmap, 0, size, prot, flags, -1, 0);
-#endif
- if (INTERNAL_SYSCALL_ERROR_P (ret))
- result = NULL;
- else
- result = (void *) ret;
- }
+ result = _dl_early_mmap (size);
return result;
}
new file mode 100644
@@ -0,0 +1,41 @@
+/* Early anonymous mmap for ld.so, before self-relocation. Linux version.
+ Copyright (C) 2022-2023 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/>. */
+
+#ifndef DL_EARLY_MMAP_H
+#define DL_EARLY_MMAP_H
+
+#include <mmap_call.h>
+
+static inline __attribute__ ((always_inline)) void *
+_dl_early_mmap (size_t size)
+{
+ long int ret;
+ int prot = PROT_READ | PROT_WRITE;
+ int flags = MAP_PRIVATE | MAP_ANONYMOUS;
+#ifdef __NR_mmap2
+ ret = MMAP_CALL_INTERNAL (mmap2, 0, size, prot, flags, -1, 0);
+#else
+ ret = MMAP_CALL_INTERNAL (mmap, 0, size, prot, flags, -1, 0);
+#endif
+ if (INTERNAL_SYSCALL_ERROR_P (ret))
+ return NULL;
+ else
+ return (void *) ret;
+}
+
+#endif /* DL_EARLY_MMAP_H */