about summary refs log tree commit diff
diff options
context:
space:
mode:
authorFlorian Weimer <fweimer@redhat.com>2024-07-02 13:19:13 +0200
committerFlorian Weimer <fweimer@redhat.com>2024-07-08 15:28:00 +0200
commita2f53a77559b610c8bf762c5d22172d9d45800e0 (patch)
treebe4fbd7a144bcb5840be71d09ef117a55794f41a
parent9fc639f654dc004736836613be703e6bed0c36a8 (diff)
downloadglibc-fw/bug31943-with-test.tar.gz
glibc-fw/bug31943-with-test.tar.xz
glibc-fw/bug31943-with-test.zip
elf: Handle ld.so with LOAD segment gaps in _dl_find_object (bug 31943) fw/bug31943-with-test
-rw-r--r--config.h.in3
-rw-r--r--config.make.in1
-rwxr-xr-xconfigure38
-rw-r--r--configure.ac20
-rw-r--r--dlfcn/tst-dlinfo-phdr.c4
-rw-r--r--elf/Makefile8
-rw-r--r--elf/dl-find_object.c76
-rw-r--r--elf/rtld.c24
-rw-r--r--elf/tst-load-segment-gaps.py74
9 files changed, 218 insertions, 30 deletions
diff --git a/config.h.in b/config.h.in
index 9a83b774fa..c8a449d620 100644
--- a/config.h.in
+++ b/config.h.in
@@ -260,6 +260,9 @@
    any external dependencies such as making a function call.  */
 #define HAVE_BUILTIN_TRAP 0
 
+/* Define if ld may produce gaps in load segments.  */
+#undef HAVE_LD_LOAD_GAPS
+
 /* ports/sysdeps/mips/configure.in  */
 /* Define if using the IEEE 754-2008 NaN encoding on the MIPS target.  */
 #undef HAVE_MIPS_NAN2008
diff --git a/config.make.in b/config.make.in
index 55e8b7563b..831db714c1 100644
--- a/config.make.in
+++ b/config.make.in
@@ -98,6 +98,7 @@ CXX = @CXX@
 BUILD_CC = @BUILD_CC@
 CFLAGS = @CFLAGS@
 CPPFLAGS-config = @CPPFLAGS@
+have-ld-load-gaps = @with_ld_load_gaps@
 extra-nonshared-cflags = @extra_nonshared_cflags@
 rtld-early-cflags = @rtld_early_cflags@
 ASFLAGS-config = @ASFLAGS_config@
diff --git a/configure b/configure
index 1bae55b45b..4685b06203 100755
--- a/configure
+++ b/configure
@@ -610,6 +610,7 @@ PACKAGE_URL='https://www.gnu.org/software/glibc/'
 
 ac_unique_file="include/features.h"
 enable_option_checking=no
+with_ld_load_gaps=check
 ac_subst_vars='LTLIBOBJS
 LIBOBJS
 pthread_in_libc
@@ -679,6 +680,7 @@ SED
 MAKEINFO
 MSGFMT
 MAKE
+with_ld_load_gaps
 LD
 NM
 OBJDUMP
@@ -807,6 +809,7 @@ enable_cet
 enable_scv
 enable_fortify_source
 with_cpu
+with_ld_load_gaps
 '
       ac_precious_vars='build_alias
 host_alias
@@ -1509,6 +1512,8 @@ Optional Packages:
   --with-timeoutfactor=NUM
                           specify an integer to scale the timeout
   --with-cpu=CPU          select code for CPU variant
+  --with-ld-load-gaps     support linker with LOAD segment gaps bug
+                          [default=check]
 
 Some influential environment variables:
   CC          C compiler command
@@ -5302,6 +5307,39 @@ esac
 config_vars="$config_vars
 with-lld = $libc_cv_with_lld"
 
+
+# Check whether --with-ld_load_gaps was given.
+if test ${with_ld_load_gaps+y}
+then :
+  withval=$with_ld_load_gaps;
+else case e in #(
+  e) :  ;;
+esac
+fi
+
+if test "x$with_ld_load_gaps" = xcheck
+then :
+  echo "Checking binutils ld version:" >&5
+   if LC_ALL=C $LD --version | grep -E '^GNU ld version 2\.(2[0-9]|3[0-8])[^0-9]' >&5
+then :
+  with_ld_load_gaps=yes
+else case e in #(
+  e) with_ld_load_gaps=no
+	 echo "(linker not binutils or not impacted)" >&5 ;;
+esac
+fi
+fi
+if test "x$with_ld_load_gaps" != xyes && test "x$with_ld_load_gaps" != xno
+then :
+  as_fn_error $? "invalid --with-ld-load-gaps argument: $with_ld_load_gaps" "$LINENO" 5
+fi
+if test "x$with_ld_load_gaps" = xyes
+then :
+  printf "%s\n" "#define HAVE_LD_LOAD_GAPS 1" >>confdefs.h
+
+fi
+
+
 # These programs are version sensitive.
 for ac_prog in gnumake gmake make
 do
diff --git a/configure.ac b/configure.ac
index e48957f318..4a952c1001 100644
--- a/configure.ac
+++ b/configure.ac
@@ -526,6 +526,26 @@ case $($LD --version) in
 esac
 LIBC_CONFIG_VAR([with-lld], [$libc_cv_with_lld])
 
+dnl Workaround for binutils LOAD segment gaps bug (swbz#28743)
+dnl Fixed in commit 9833b7757d246f22db4eb24b8e5db7eb5e05b6d9
+dnl ("PR28824, relro security issues"), part of binutils 2.39.
+AC_ARG_WITH([ld_load_gaps],
+  [AS_HELP_STRING([--with-ld-load-gaps],
+    [support linker with LOAD segment gaps bug @<:@default=check@:>@])],
+  [],
+  [: m4_divert_text([DEFAULTS], [with_ld_load_gaps=check])])
+AS_IF([test "x$with_ld_load_gaps" = xcheck],
+  [echo "Checking binutils ld version:" >&AS_MESSAGE_LOG_FD
+   AS_IF([LC_ALL=C $LD --version | grep -E '^GNU ld version 2\.(2[[0-9]]|3[[0-8]])[[^0-9]]' >&AS_MESSAGE_LOG_FD],
+        [with_ld_load_gaps=yes],
+	[with_ld_load_gaps=no
+	 echo "(linker not binutils or not impacted)" >&AS_MESSAGE_LOG_FD])])
+AS_IF([test "x$with_ld_load_gaps" != xyes && test "x$with_ld_load_gaps" != xno],
+  AC_MSG_ERROR([invalid --with-ld-load-gaps argument: $with_ld_load_gaps]))
+AS_IF([test "x$with_ld_load_gaps" = xyes],
+  [AC_DEFINE(HAVE_LD_LOAD_GAPS)])
+AC_SUBST(with_ld_load_gaps)
+
 # These programs are version sensitive.
 AC_CHECK_PROG_VER(MAKE, gnumake gmake make, --version,
   [GNU Make[^0-9]*\([0-9][0-9.]*\)],
diff --git a/dlfcn/tst-dlinfo-phdr.c b/dlfcn/tst-dlinfo-phdr.c
index fdffb17724..7da525a1e3 100644
--- a/dlfcn/tst-dlinfo-phdr.c
+++ b/dlfcn/tst-dlinfo-phdr.c
@@ -64,8 +64,8 @@ do_test (void)
 
   do
     {
-      printf ("info: checking link map %p (%p) for \"%s\"\n",
-              l, l->l_phdr, l->l_name);
+      printf ("info: checking link map %p (%p %p) for \"%s\"\n",
+              l, l->l_phdr, (void *) l->l_addr, l->l_name);
 
       /* Cause dlerror () to return an error message.  */
       dlsym (RTLD_DEFAULT, "does-not-exist");
diff --git a/elf/Makefile b/elf/Makefile
index a3475f3fb5..a9fd97e96e 100644
--- a/elf/Makefile
+++ b/elf/Makefile
@@ -615,6 +615,14 @@ $(objpfx)tst-relro-libc.out: tst-relro-symbols.py $(..)/scripts/glibcelf.py \
 	    --required=_IO_wfile_jumps \
 	    --required=__io_vtables \
 	  > $@ 2>&1; $(evaluate-test)
+tests-special += $(objpfx)tst-gaps-ldso.out
+$(objpfx)tst-gaps-ldso.out: tst-load-segment-gaps.py \
+  $(..)/scripts/glibcelf.py $(objpfx)ld.so
+	$(PYTHON) tst-load-segment-gaps.py $(objpfx)ld.so \
+	  > $@ 2>&1; $(evaluate-test)
+ifeq ($(have-ld-load-gaps),yes)
+test-xfail-tst-gaps-ldso = yes
+endif
 
 ifeq ($(run-built-tests),yes)
 tests-special += $(objpfx)tst-valgrind-smoke.out
diff --git a/elf/dl-find_object.c b/elf/dl-find_object.c
index 449302eda3..9b63e7bd92 100644
--- a/elf/dl-find_object.c
+++ b/elf/dl-find_object.c
@@ -466,6 +466,38 @@ __dl_find_object (void *pc1, struct dl_find_object *result)
 hidden_def (__dl_find_object)
 weak_alias (__dl_find_object, _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;
+            _dlfo_nodelete_mappings[nodelete].map_start
+              = ph->p_vaddr + map->l_addr;
+            _dlfo_nodelete_mappings[nodelete].map_end
+              = _dlfo_nodelete_mappings[nodelete].map_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
@@ -477,29 +509,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)
@@ -511,11 +522,20 @@ _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;
+#if defined HAVE_LD_LOAD_GAPS && defined SHARED
+              /* The kernel may have loaded ld.so with gaps.   */
+              if (!l->l_contiguous && l == &GL(dl_rtld_map))
+                nodelete
+                  = _dlfo_process_initial_noncontiguous_map (l, nodelete);
+              else
+#endif
+                {
+                  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)
             {
diff --git a/elf/rtld.c b/elf/rtld.c
index bfdf632e77..337901007d 100644
--- a/elf/rtld.c
+++ b/elf/rtld.c
@@ -1766,6 +1766,30 @@ dl_main (const ElfW(Phdr) *phdr,
 
   GL(dl_rtld_map).l_phdr = rtld_phdr;
   GL(dl_rtld_map).l_phnum = rtld_ehdr->e_phnum;
+  GL(dl_rtld_map).l_contiguous = 1;
+#ifdef HAVE_LD_LOAD_GAPS
+  /* 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.  */
+  {
+    ElfW(Addr) expected_load_address = 0;
+    for (const ElfW(Phdr) *ph = rtld_phdr; ph < &phdr[rtld_ehdr->e_phnum];
+	 ++ph)
+      if (ph->p_type == PT_LOAD)
+	{
+	  ElfW(Addr) mapstart = ph->p_vaddr & ~(GLRO(dl_pagesize) - 1);
+	  if (GL(dl_rtld_map).l_contiguous && expected_load_address != 0
+	      && expected_load_address != mapstart)
+	    GL(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));
+	}
+  }
+#endif
 
 
   /* PT_GNU_RELRO is usually the last phdr.  */
diff --git a/elf/tst-load-segment-gaps.py b/elf/tst-load-segment-gaps.py
new file mode 100644
index 0000000000..85ae7344fd
--- /dev/null
+++ b/elf/tst-load-segment-gaps.py
@@ -0,0 +1,74 @@
+#!/usr/bin/python3
+# Verify that objects do not contain gaps in load segments.
+# Copyright (C) 2024 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/>.
+
+import argparse
+import os.path
+import sys
+
+# Make available glibc Python modules.
+sys.path.append(os.path.join(
+    os.path.dirname(os.path.realpath(__file__)), os.path.pardir, 'scripts'))
+
+import glibcelf
+
+def rounddown(val, align):
+    assert (align & (align - 1)) == 0, align
+    return val & -align
+def roundup(val, align):
+    assert (align & (align - 1)) == 0, align
+    return (val + align - 1) & -align
+
+errors = False
+
+def process(path, img):
+    global errors
+    loads = [phdr for phdr in img.phdrs()
+             if phdr.p_type == glibcelf.Pt.PT_LOAD]
+    if not loads:
+        # Nothing ot check.
+        return
+    alignments = [phdr.p_align for phdr in loads if phdr.p_align > 0]
+    if alignments:
+        align = min(alignments)
+    else:
+        print('error: cannot infer page size')
+        errors = True
+        align = 4096
+    print('info: inferred page size:', align)
+    current_address = None
+    for idx, phdr in enumerate(loads):
+        this_address = rounddown(phdr.p_vaddr, align)
+        next_address = roundup(phdr.p_vaddr + phdr.p_memsz, align)
+        print('info: LOAD segment {}: address 0x{:x}, size {},'
+              ' range [0x{:x},0x{:x})'
+               .format(idx, phdr.p_vaddr, phdr.p_memsz,
+                       this_address, next_address))
+        if current_address is not None:
+            gap = this_address - current_address
+            if gap != 0:
+                errors = True
+                print('error: gap between load segments: {} bytes'.format(gap))
+        current_address = next_address
+
+for path in sys.argv[1:]:
+    img = glibcelf.Image.readfile(path)
+    process(path, img)
+
+if errors:
+    sys.exit(1)