[PATCH v2 2/2] elf: Handle ld.so with LOAD segment gaps in _dl_find_object (bug 31943)
Adhemerval Zanella Netto
adhemerval.zanella@linaro.org
Thu Jul 31 20:47:18 GMT 2025
On 25/07/25 08:10, Florian Weimer wrote:
> Detect if ld.so not contiguous and handle that case in _dl_find_object.
> Set l_find_object_processed even for initially loaded link maps,
> otherwise dlopen of an initially loaded object adds it to
> _dlfo_loaded_mappings (where maps are expected to be contiguous),
> in addition to _dlfo_nodelete_mappings.
>
> Test elf/tst-link-map-contiguous-ldso iterates over the loader
> image, reading every word to make sure memory is actually mapped.
> It only does that if the l_contiguous flag is set for the link map.
> Otherwise, it finds gaps with mmap and checks that _dl_find_object
> does not return the ld.so mapping for them.
>
> The test elf/tst-link-map-contiguous-main does the same thing for
> the libc.so shared object. This only works if the kernel loaded
> the main program because the glibc dynamic loader may fill
> the gaps with PROT_NONE mappings in some cases, making it contiguous,
> but accesses to individual words may still fault.
>
> Test elf/tst-link-map-contiguous-libc is again slightly different
> because the dynamic loader always fills the gaps with PROT_NONE
> mappings, so a different form of probing has to be used.
The patch looks ok, just some comments below mainly about testing.
I tested with a build using -Wl,-z,max-page-size=0x200000, which
triggers non-contiguous ldso and binaries on x86_64, and I saw
not regressions (I have not tested all the possible combinations,
but the patch logic is sound).
I just realized that we do not track auxiliary mapping done by the
kernel, like [vvar] and [vsyscall]; but it should not really matter
for the ABI (although for old binaries some syscall might be done
through vsyscall).
Reviewed-by: Adhemerval Zanella <adhemerval.zanella@linaro.org>
> ---
> elf/Makefile | 6 ++
> elf/dl-find_object.c | 74 +++++++++++++---------
> elf/dl-find_object.h | 7 ++-
> elf/rtld.c | 25 +++++++-
> elf/tst-link-map-contiguous-ldso.c | 98 ++++++++++++++++++++++++++++++
> elf/tst-link-map-contiguous-libc.c | 57 +++++++++++++++++
> elf/tst-link-map-contiguous-main.c | 45 ++++++++++++++
> 7 files changed, 280 insertions(+), 32 deletions(-)
> create mode 100644 elf/tst-link-map-contiguous-ldso.c
> create mode 100644 elf/tst-link-map-contiguous-libc.c
> create mode 100644 elf/tst-link-map-contiguous-main.c
>
> diff --git a/elf/Makefile b/elf/Makefile
> index 44b9f192f2..35633278a3 100644
> --- a/elf/Makefile
> +++ b/elf/Makefile
> @@ -542,6 +542,8 @@ tests-internal += \
> tst-dl_find_object-threads \
> tst-dlmopen2 \
> tst-hash-collision3 \
> + tst-link-map-contiguous-ldso \
> + tst-link-map-contiguous-libc \
> tst-ptrguard1 \
> tst-stackguard1 \
> tst-tls-surplus \
> @@ -553,6 +555,10 @@ tests-internal += \
> unload2 \
> # tests-internal
>
> +ifeq ($(build-hardcoded-path-in-tests),yes)
> +tests-internal += tst-link-map-contiguous-main
> +endif
> +
> tests-container += \
> tst-dlopen-self-container \
> tst-dlopen-tlsmodid-container \
> diff --git a/elf/dl-find_object.c b/elf/dl-find_object.c
> index 1e76373292..c9f4c1c8d1 100644
> --- a/elf/dl-find_object.c
> +++ b/elf/dl-find_object.c
> @@ -465,6 +465,37 @@ _dl_find_object (void *pc1, struct dl_find_object *result)
> }
> rtld_hidden_def (_dl_find_object)
>
> +/* Subroutine of _dlfo_process_initial to split out noncontigous link
I think we usually write this as 'non-contiguous', as for other occurrences
in malloc and dl-find_object.c itself.
> + maps. NODELETE is the number of used _dlfo_nodelete_mappings
> + elements. It is incremented as needed, and the new NODELETE value
> + is returned. */
> +static size_t
> +_dlfo_process_initial_noncontiguous_map (struct link_map *map,
> + size_t nodelete)
> +{
> + struct dl_find_object_internal dlfo;
> + _dl_find_object_from_map (map, &dlfo);
> +
> + /* PT_LOAD segments for a non-contiguous link map are added to the
> + non-closeable mappings. */
> + const ElfW(Phdr) *ph = map->l_phdr;
> + const ElfW(Phdr) *ph_end = map->l_phdr + map->l_phnum;
> + for (; ph < ph_end; ++ph)
> + if (ph->p_type == PT_LOAD)
> + {
> + if (_dlfo_nodelete_mappings != NULL)
> + {
> + /* Second pass only. */
> + _dlfo_nodelete_mappings[nodelete] = dlfo;
> + ElfW(Addr) start = ph->p_vaddr + map->l_addr;
> + _dlfo_nodelete_mappings[nodelete].map_start = start;
> + _dlfo_nodelete_mappings[nodelete].map_end = start + ph->p_memsz;
> + }
> + ++nodelete;
> + }
> + return nodelete;
> +}
> +
Ok.
> /* _dlfo_process_initial is called twice. First to compute the array
> sizes from the initial loaded mappings. Second to fill in the
> bases and infos arrays with the (still unsorted) data. Returns the
> @@ -476,29 +507,8 @@ _dlfo_process_initial (void)
>
> size_t nodelete = 0;
> if (!main_map->l_contiguous)
> - {
> - struct dl_find_object_internal dlfo;
> - _dl_find_object_from_map (main_map, &dlfo);
> -
> - /* PT_LOAD segments for a non-contiguous are added to the
> - non-closeable mappings. */
> - for (const ElfW(Phdr) *ph = main_map->l_phdr,
> - *ph_end = main_map->l_phdr + main_map->l_phnum;
> - ph < ph_end; ++ph)
> - if (ph->p_type == PT_LOAD)
> - {
> - if (_dlfo_nodelete_mappings != NULL)
> - {
> - /* Second pass only. */
> - _dlfo_nodelete_mappings[nodelete] = dlfo;
> - _dlfo_nodelete_mappings[nodelete].map_start
> - = ph->p_vaddr + main_map->l_addr;
> - _dlfo_nodelete_mappings[nodelete].map_end
> - = _dlfo_nodelete_mappings[nodelete].map_start + ph->p_memsz;
> - }
> - ++nodelete;
> - }
> - }
> + /* Contiguous case already handled in _dl_find_object_init. */
> + nodelete = _dlfo_process_initial_noncontiguous_map (main_map, nodelete);
>
> size_t loaded = 0;
> for (Lmid_t ns = 0; ns < GL(dl_nns); ++ns)
> @@ -510,11 +520,18 @@ _dlfo_process_initial (void)
> /* lt_library link maps are implicitly NODELETE. */
> if (l->l_type == lt_library || l->l_nodelete_active)
> {
> - if (_dlfo_nodelete_mappings != NULL)
> - /* Second pass only. */
> - _dl_find_object_from_map
> - (l, _dlfo_nodelete_mappings + nodelete);
> - ++nodelete;
> + /* The kernel may have loaded ld.so with gaps. */
> + if (!l->l_contiguous && is_rtld_link_map (l))
> + nodelete
> + = _dlfo_process_initial_noncontiguous_map (l, nodelete);
> + else
> + {
> + if (_dlfo_nodelete_mappings != NULL)
> + /* Second pass only. */
> + _dl_find_object_from_map
> + (l, _dlfo_nodelete_mappings + nodelete);
> + ++nodelete;
> + }
> }
> else if (l->l_type == lt_loaded)
> {
Ok.
> @@ -764,7 +781,6 @@ _dl_find_object_update_1 (struct link_map **loaded, size_t count)
> /* Prefer newly loaded link map. */
> assert (loaded_index1 > 0);
> _dl_find_object_from_map (loaded[loaded_index1 - 1], dlfo);
> - loaded[loaded_index1 - 1]->l_find_object_processed = 1;
> --loaded_index1;
> }
>
Ok.
> diff --git a/elf/dl-find_object.h b/elf/dl-find_object.h
> index 9aa2439eaa..d9d75c4ad9 100644
> --- a/elf/dl-find_object.h
> +++ b/elf/dl-find_object.h
> @@ -94,7 +94,7 @@ _dl_find_object_to_external (struct dl_find_object_internal *internal,
> }
>
> /* Extract the object location data from a link map and writes it to
> - *RESULT using relaxed MO stores. */
> + *RESULT using relaxed MO stores. Set L->l_find_object_processed. */
> static void __attribute__ ((unused))
> _dl_find_object_from_map (struct link_map *l,
> struct dl_find_object_internal *result)
> @@ -141,8 +141,11 @@ _dl_find_object_from_map (struct link_map *l,
> break;
> }
> if (read_seg == 3)
> - return;
> + goto done;
> }
> +
> + done:
> + l->l_find_object_processed = 1;
> }
>
Ok.
> /* Called by the dynamic linker to set up the data structures for the
> diff --git a/elf/rtld.c b/elf/rtld.c
> index 6fb900fb31..ef4d96c053 100644
> --- a/elf/rtld.c
> +++ b/elf/rtld.c
> @@ -1241,7 +1241,7 @@ rtld_setup_main_map (struct link_map *main_map)
>
> /* Set up the program header information for the dynamic linker
> itself. It can be accessed via _r_debug and dl_iterate_phdr
> - callbacks. */
> + callbacks, and it is used by _dl_find_object. */
> static void
> rtld_setup_phdr (void)
> {
> @@ -1259,6 +1259,29 @@ rtld_setup_phdr (void)
> _dl_rtld_map.l_phnum = rtld_ehdr->e_phnum;
>
>
> + _dl_rtld_map.l_contiguous = 1;
> + /* The linker may not have produced a contiguous object. The kernel
> + will load the object with actual gaps (unlike the glibc loader
> + for shared objects, which always produces a contiguous mapping).
> + See similar logic in rtld_setup_main_map above. */
> + {
> + ElfW(Addr) expected_load_address = 0;
> + for (const ElfW(Phdr) *ph = rtld_phdr; ph < &rtld_phdr[rtld_ehdr->e_phnum];
> + ++ph)
> + if (ph->p_type == PT_LOAD)
> + {
> + ElfW(Addr) mapstart = ph->p_vaddr & ~(GLRO(dl_pagesize) - 1);
Maybe ALIGN_DOWN here?
> + if (_dl_rtld_map.l_contiguous && expected_load_address != 0
> + && expected_load_address != mapstart)
> + _dl_rtld_map.l_contiguous = 0;
> + ElfW(Addr) allocend = ph->p_vaddr + ph->p_memsz;
> + /* The next expected address is the page following this load
> + segment. */
> + expected_load_address = ((allocend + GLRO(dl_pagesize) - 1)
> + & ~(GLRO(dl_pagesize) - 1));
Maybe ALIGN_UP?
> + }
> + }
> +
> /* PT_GNU_RELRO is usually the last phdr. */
> size_t cnt = rtld_ehdr->e_phnum;
> while (cnt-- > 0)
> diff --git a/elf/tst-link-map-contiguous-ldso.c b/elf/tst-link-map-contiguous-ldso.c
> new file mode 100644
> index 0000000000..04de808bb2
> --- /dev/null
> +++ b/elf/tst-link-map-contiguous-ldso.c
> @@ -0,0 +1,98 @@
> +/* Check that _dl_find_object behavior matches up with gaps.
> + 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 <dlfcn.h>
> +#include <gnu/lib-names.h>
> +#include <link.h>
> +#include <stdbool.h>
> +#include <stdio.h>
> +#include <support/check.h>
> +#include <support/xdlfcn.h>
> +#include <support/xunistd.h>
> +#include <sys/mman.h>
> +#include <unistd.h>
> +
> +static int
> +do_test (void)
> +{
> + struct link_map *l = xdlopen (LD_SO, RTLD_NOW);
> + if (!l->l_contiguous)
> + {
> + puts ("info: ld.so link map is not contiguous");
> +
> + /* Try to find holes by probing with mmap. */
> + int pagesize = getpagesize ();
> + bool gap_found = false;
> + ElfW(Addr) addr = l->l_map_start;
> + TEST_COMPARE (addr % pagesize, 0);
> + while (addr < l->l_map_end)
> + {
> + void *expected = (void *) addr;
> + void *ptr = xmmap (expected, 1, PROT_READ | PROT_WRITE,
> + MAP_PRIVATE | MAP_ANONYMOUS, -1);
> + struct dl_find_object dlfo;
> + int dlfo_ret = _dl_find_object (expected, &dlfo);
> + if (ptr == expected)
> + {
> + if (dlfo_ret < 0)
> + {
> + TEST_COMPARE (dlfo_ret, -1);
> + printf ("info: hole without mapping data found at %p\n", ptr);
> + }
> + else
> + FAIL ("object \"%s\" found in gap at %p",
> + dlfo.dlfo_link_map->l_name, ptr);
> + gap_found = true;
> + }
> + else if (dlfo_ret == 0)
> + {
> + if ((void *) dlfo.dlfo_link_map != (void *) l)
> + {
> + printf ("info: object \"%s\" found at %p\n",
> + dlfo.dlfo_link_map->l_name, ptr);
> + gap_found = true;
> + }
> + }
> + else
> + TEST_COMPARE (dlfo_ret, -1);
> + xmunmap (ptr, 1);
> + addr += pagesize;
> + }
> + if (!gap_found)
> + FAIL ("no ld.so gap found");
> + }
> + else
> + {
> + puts ("info: ld.so link map is contiguous");
> +
> + /* Assert that ld.so is truly contiguous in memory. */
> + volatile long int *p = (volatile long int *) l->l_map_start;
> + volatile long int *end = (volatile long int *) l->l_map_end;
> + while (p < end)
> + {
> + *p;
> + ++p;
> + }
> + }
> +
> + xdlclose (l);
> +
> + return 0;
> +}
> +
> +#include <support/test-driver.c>
> diff --git a/elf/tst-link-map-contiguous-libc.c b/elf/tst-link-map-contiguous-libc.c
> new file mode 100644
> index 0000000000..eb5728c765
> --- /dev/null
> +++ b/elf/tst-link-map-contiguous-libc.c
> @@ -0,0 +1,57 @@
> +/* Check that the entire libc.so program image is readable if contiguous.
> + 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 <gnu/lib-names.h>
> +#include <link.h>
> +#include <support/check.h>
> +#include <support/xdlfcn.h>
> +#include <support/xunistd.h>
> +#include <sys/mman.h>
> +#include <unistd.h>
> +
> +static int
> +do_test (void)
> +{
> + struct link_map *l = xdlopen (LIBC_SO, RTLD_NOW);
> +
> + /* The dynamic loader fills holes with PROT_NONE mappings. */
> + if (!l->l_contiguous)
> + FAIL_EXIT1 ("libc.so link map is not contiguous");
> +
> + /* Direct probing does not work because not everything is readable
> + due to PROT_NONE mappings. */
> + int pagesize = getpagesize ();
> + ElfW(Addr) addr = l->l_map_start;
> + TEST_COMPARE (addr % pagesize, 0);
> + while (addr < l->l_map_end)
> + {
> + void *expected = (void *) addr;
> + void *ptr = xmmap (expected, 1, PROT_READ | PROT_WRITE,
> + MAP_PRIVATE | MAP_ANONYMOUS, -1);
Is this true for all possible Linux configuration that mmap will always
try to allocate near the input address? What about Hurd and should we
make it Linux specific?
> + if (ptr == expected)
> + FAIL ("hole in libc.so memory image after %lu bytes",
> + (unsigned long int) (addr - l->l_map_start));
> + xmunmap (ptr, 1);
> + addr += pagesize;
> + }
> +
> + xdlclose (l);
> +
> + return 0;
> +}
> +#include <support/test-driver.c>
> diff --git a/elf/tst-link-map-contiguous-main.c b/elf/tst-link-map-contiguous-main.c
> new file mode 100644
> index 0000000000..2d1a054f0f
> --- /dev/null
> +++ b/elf/tst-link-map-contiguous-main.c
> @@ -0,0 +1,45 @@
> +/* Check that the entire main program image is readable if contiguous.
> + 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 <link.h>
> +#include <support/check.h>
> +#include <support/xdlfcn.h>
> +
> +static int
> +do_test (void)
> +{
> + struct link_map *l = xdlopen ("", RTLD_NOW);
> + if (!l->l_contiguous)
> + FAIL_UNSUPPORTED ("main link map is not contiguous");
> +
> + /* This check only works if the kernel loaded the main program. The
> + dynamic loader replaces gaps with PROT_NONE mappings, resulting
> + in faults. */
> + volatile long int *p = (volatile long int *) l->l_map_start;
> + volatile long int *end = (volatile long int *) l->l_map_end;
> + while (p < end)
> + {
> + *p;
> + ++p;
> + }
> +
> + xdlclose (l);
> +
> + return 0;
> +}
> +#include <support/test-driver.c>
More information about the Libc-alpha
mailing list