[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