]> git.ipfire.org Git - thirdparty/glibc.git/commitdiff
elf: Handle ld.so with LOAD segment gaps in _dl_find_object (bug 31943)
authorFlorian Weimer <fweimer@redhat.com>
Fri, 1 Aug 2025 08:20:23 +0000 (10:20 +0200)
committerFlorian Weimer <fweimer@redhat.com>
Fri, 1 Aug 2025 08:21:39 +0000 (10:21 +0200)
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.

Reviewed-by: Adhemerval Zanella <adhemerval.zanella@linaro.org>
elf/Makefile
elf/dl-find_object.c
elf/dl-find_object.h
elf/rtld.c
elf/tst-link-map-contiguous-ldso.c [new file with mode: 0644]
elf/tst-link-map-contiguous-libc.c [new file with mode: 0644]
elf/tst-link-map-contiguous-main.c [new file with mode: 0644]

index 48aa0b57e5b3fb488bd4135e799678e94b568e22..3a5596e2bbc9f8ff77cb30401d203b351c7bc28c 100644 (file)
@@ -543,6 +543,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 \
@@ -554,6 +556,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 \
index 1e7637329257f2c00e61419a5d567f5aeae20870..c9f4c1c8d11d27ff0a7aee6f0ec98014e4ad035c 100644 (file)
@@ -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
+   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;
+}
+
 /* _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)
             {
@@ -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;
         }
 
index 9aa2439eaa64807c1e7d9a3d386f482db51ea421..d9d75c4ad9d5784c9d7420fe62cc9822099683e9 100644 (file)
@@ -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;
 }
 
 /* Called by the dynamic linker to set up the data structures for the
index 6fb900fb31a15ac481d6a5d4c4f1f15fc41e7440..ef4d96c053f5e75b3e727fb36063370fa66829e7 100644 (file)
@@ -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);
+         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));
+       }
+  }
+
   /* 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 (file)
index 0000000..04de808
--- /dev/null
@@ -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 (file)
index 0000000..eb5728c
--- /dev/null
@@ -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);
+      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 (file)
index 0000000..2d1a054
--- /dev/null
@@ -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>