elf: Handle ld.so with LOAD segment gaps in _dl_find_object (bug 31943)

This commit is contained in:
Florian Weimer 2024-07-02 13:19:13 +02:00
parent 9fc639f654
commit a2f53a7755
9 changed files with 218 additions and 30 deletions

View File

@ -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

View File

@ -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@

38
configure vendored
View File

@ -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

View File

@ -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.]*\)],

View File

@ -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");

View File

@ -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

View File

@ -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)
{

View File

@ -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. */

View File

@ -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)