elf: Fix DFS sorting algorithm for LD_TRACE_LOADED_OBJECTS with missing libraries (BZ #28868)

On _dl_map_object the underlying file is not opened in trace mode
(in other cases where the underlying file can't be opened,
_dl_map_object  quits with an error).  If there any missing libraries
being processed, they will not be considered on final nlist size
passed on _dl_sort_maps later in the function.  And it is then used by
_dl_sort_maps_dfs on the stack allocated working maps:

222   /* Array to hold RPO sorting results, before we copy back to  maps[].  */
223   struct link_map *rpo[nmaps];
224
225   /* The 'head' position during each DFS iteration. Note that we start at
226      one past the last element due to first-decrement-then-store (see the
227      bottom of above dfs_traversal() routine).  */
228   struct link_map **rpo_head = &rpo[nmaps];

However while transversing the 'l_initfini' on dfs_traversal it will
still consider the l_faked maps and thus update rpo more times than the
allocated working 'rpo', overflowing the stack object.

As suggested in bugzilla, one option would be to avoid sorting the maps
for trace mode.  However I think ignoring l_faked object does make
sense (there is one less constraint to call the sorting function), it
allows a slight less stack usage for trace, and it is slight simpler
solution.

The tests does trigger the stack overflow, however I tried to make
it more generic to check different scenarios or missing objects.

Checked on x86_64-linux-gnu.

Reviewed-by: Siddhesh Poyarekar <siddhesh@sourceware.org>
This commit is contained in:
Adhemerval Zanella 2022-02-08 15:22:49 -03:00
parent 4f7b7d00e0
commit 3a0588ae48
14 changed files with 200 additions and 1 deletions

View File

@ -693,6 +693,11 @@ modules-names += \
libmarkermod5-3 \
libmarkermod5-4 \
libmarkermod5-5 \
libtracemod1-1 \
libtracemod2-1 \
libtracemod3-1 \
libtracemod4-1 \
libtracemod5-1 \
ltglobmod1 \
ltglobmod2 \
neededobj1 \
@ -1161,6 +1166,11 @@ tests-special += \
$(objpfx)tst-initorder2-cmp.out \
$(objpfx)tst-unused-dep-cmp.out \
$(objpfx)tst-unused-dep.out \
$(objpfx)tst-trace1.out \
$(objpfx)tst-trace2.out \
$(objpfx)tst-trace3.out \
$(objpfx)tst-trace4.out \
$(objpfx)tst-trace5.out \
# tests-special
endif
@ -2884,3 +2894,47 @@ $(objpfx)tst-relr-mod4a.so: $(objpfx)tst-relr-mod4a.os \
-shared -o $@.new $(filter-out $(map-file),$^)
$(call after-link,$@.new)
mv -f $@.new $@
LDFLAGS-libtracemod1-1.so += -Wl,-soname,libtracemod1.so
LDFLAGS-libtracemod2-1.so += -Wl,-soname,libtracemod2.so
LDFLAGS-libtracemod3-1.so += -Wl,-soname,libtracemod3.so
LDFLAGS-libtracemod4-1.so += -Wl,-soname,libtracemod4.so
LDFLAGS-libtracemod5-1.so += -Wl,-soname,libtracemod5.so
$(objpfx)libtracemod1-1.so: $(objpfx)libtracemod2-1.so \
$(objpfx)libtracemod3-1.so
$(objpfx)libtracemod2-1.so: $(objpfx)libtracemod4-1.so \
$(objpfx)libtracemod5-1.so
define libtracemod-x
$(objpfx)libtracemod$(1)/libtracemod$(1).so: $(objpfx)libtracemod$(1)-1.so
$$(make-target-directory)
cp $$< $$@
endef
libtracemod-suffixes = 1 2 3 4 5
$(foreach i,$(libtracemod-suffixes), $(eval $(call libtracemod-x,$(i))))
define tst-trace-skeleton
$(objpfx)tst-trace$(1).out: $(objpfx)libtracemod1/libtracemod1.so \
$(objpfx)libtracemod2/libtracemod2.so \
$(objpfx)libtracemod3/libtracemod3.so \
$(objpfx)libtracemod4/libtracemod4.so \
$(objpfx)libtracemod5/libtracemod5.so \
$(..)scripts/tst-ld-trace.py \
tst-trace$(1).exp
${ $(PYTHON) $(..)scripts/tst-ld-trace.py \
"$(test-wrapper-env) $(elf-objpfx)$(rtld-installed-name) \
--library-path $(common-objpfx):$(strip $(2)) \
$(objpfx)libtracemod1/libtracemod1.so" tst-trace$(1).exp \
} > $$@; $$(evaluate-test)
endef
$(eval $(call tst-trace-skeleton,1,))
$(eval $(call tst-trace-skeleton,2,\
$(objpfx)libtracemod2))
$(eval $(call tst-trace-skeleton,3,\
$(objpfx)libtracemod2:$(objpfx)libtracemod3))
$(eval $(call tst-trace-skeleton,4,\
$(objpfx)libtracemod2:$(objpfx)libtracemod3:$(objpfx)libtracemod4))
$(eval $(call tst-trace-skeleton,5,\
$(objpfx)libtracemod2:$(objpfx)libtracemod3:$(objpfx)libtracemod4:$(objpfx)libtracemod5))

View File

@ -473,6 +473,8 @@ _dl_map_object_deps (struct link_map *map,
for (nlist = 0, runp = known; runp; runp = runp->next)
{
/* _dl_sort_maps ignores l_faked object, so it is safe to not consider
them for nlist. */
if (__builtin_expect (trace_mode, 0) && runp->map->l_faked)
/* This can happen when we trace the loading. */
--map->l_searchlist.r_nlist;

View File

@ -140,7 +140,9 @@ static void
dfs_traversal (struct link_map ***rpo, struct link_map *map,
bool *do_reldeps)
{
if (map->l_visited)
/* _dl_map_object_deps ignores l_faked objects when calculating the
number of maps before calling _dl_sort_maps, ignore them as well. */
if (map->l_visited || map->l_faked)
return;
map->l_visited = 1;

1
elf/libtracemod1-1.c Normal file
View File

@ -0,0 +1 @@
/* Empty */

1
elf/libtracemod2-1.c Normal file
View File

@ -0,0 +1 @@
/* Empty */

1
elf/libtracemod3-1.c Normal file
View File

@ -0,0 +1 @@
/* Empty */

1
elf/libtracemod4-1.c Normal file
View File

@ -0,0 +1 @@
/* Empty */

1
elf/libtracemod5-1.c Normal file
View File

@ -0,0 +1 @@
/* Empty */

4
elf/tst-trace1.exp Normal file
View File

@ -0,0 +1,4 @@
ld 1
libc 1
libtracemod2.so 0
libtracemod3.so 0

6
elf/tst-trace2.exp Normal file
View File

@ -0,0 +1,6 @@
ld 1
libc 1
libtracemod2.so 1
libtracemod3.so 0
libtracemod4.so 0
libtracemod5.so 0

6
elf/tst-trace3.exp Normal file
View File

@ -0,0 +1,6 @@
ld 1
libc 1
libtracemod2.so 1
libtracemod3.so 1
libtracemod4.so 0
libtracemod5.so 0

6
elf/tst-trace4.exp Normal file
View File

@ -0,0 +1,6 @@
ld 1
libc 1
libtracemod2.so 1
libtracemod3.so 1
libtracemod4.so 1
libtracemod5.so 0

6
elf/tst-trace5.exp Normal file
View File

@ -0,0 +1,6 @@
ld 1
libc 1
libtracemod2.so 1
libtracemod3.so 1
libtracemod4.so 1
libtracemod5.so 1

108
scripts/tst-ld-trace.py Executable file
View File

@ -0,0 +1,108 @@
#!/usr/bin/python3
# Dump the output of LD_TRACE_LOADED_OBJECTS in architecture neutral format.
# Copyright (C) 2022 Free Software Foundation, Inc.
# Copyright The GNU Toolchain Authors.
# 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
import subprocess
import sys
try:
subprocess.run
except:
class _CompletedProcess:
def __init__(self, args, returncode, stdout=None, stderr=None):
self.args = args
self.returncode = returncode
self.stdout = stdout
self.stderr = stderr
def _run(*popenargs, input=None, timeout=None, check=False, **kwargs):
assert(timeout is None)
with subprocess.Popen(*popenargs, **kwargs) as process:
try:
stdout, stderr = process.communicate(input)
except:
process.kill()
process.wait()
raise
returncode = process.poll()
if check and returncode:
raise subprocess.CalledProcessError(returncode, popenargs)
return _CompletedProcess(popenargs, returncode, stdout, stderr)
subprocess.run = _run
def is_vdso(lib):
return lib.startswith('linux-gate') or lib.startswith('linux-vdso')
def parse_trace(cmd, fref):
new_env = os.environ.copy()
new_env['LD_TRACE_LOADED_OBJECTS'] = '1'
trace_out = subprocess.run(cmd, stdout=subprocess.PIPE, check=True,
universal_newlines=True, env=new_env).stdout
trace = []
for line in trace_out.splitlines():
line = line.strip()
if is_vdso(line):
continue
fields = line.split('=>' if '=>' in line else ' ')
lib = os.path.basename(fields[0].strip())
if lib.startswith('ld'):
lib = 'ld'
elif lib.startswith('libc'):
lib = 'libc'
found = 1 if fields[1].strip() != 'not found' else 0
trace += ['{} {}'.format(lib, found)]
trace = sorted(trace)
reference = sorted(line.replace('\n','') for line in fref.readlines())
ret = 0 if trace == reference else 1
if ret != 0:
for i in reference:
if i not in trace:
print("Only in {}: {}".format(fref.name, i))
for i in trace:
if i not in reference:
print("Only in trace: {}".format(i))
sys.exit(ret)
def get_parser():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('command',
help='comand to run')
parser.add_argument('reference',
help='reference file to compare')
return parser
def main(argv):
parser = get_parser()
opts = parser.parse_args(argv)
with open(opts.reference, 'r') as fref:
# Remove the initial 'env' command.
parse_trace(opts.command.split()[1:], fref)
if __name__ == '__main__':
main(sys.argv[1:])