Commit 5cefe319 authored by Jérome Perrin's avatar Jérome Perrin

testing/testcase: check executables are not linked with system libraries

Except a short whitelist of system libraries, we should have always set
rpath so that the slapos version of the library is used.

We had issues with components which where compiled with slapos version
of openssl but linked without rpath. At runtime they were using system
openssl.

Rewrite our complex grep shell pipeline in python, with a function
parsing ldd output so that we can check not only for not found dynamic
libraries, but also for libraries that are found outside of slapos
library dirs.

This also fixes problems that this grep pipline did not support paths
with spaces in their names.
parent 28e07ee6
...@@ -30,6 +30,7 @@ import unittest ...@@ -30,6 +30,7 @@ import unittest
import os import os
import fnmatch import fnmatch
import glob import glob
import re
import logging import logging
import shutil import shutil
from six.moves.urllib.parse import urlparse from six.moves.urllib.parse import urlparse
...@@ -150,54 +151,140 @@ def makeModuleSetUpAndTestCaseClass( ...@@ -150,54 +151,140 @@ def makeModuleSetUpAndTestCaseClass(
unittest.installHandler() unittest.installHandler()
logging.basicConfig( logging.basicConfig(
level=logging.DEBUG if (verbose or debug) else logging.WARNING) level=logging.DEBUG if (verbose or debug) else logging.WARNING)
installSoftwareUrlList(cls, [software_url], debug=debug) installSoftwareUrlList(cls, [software_url], shared_part_list, debug=debug)
return setUpModule, SlapOSInstanceTestCase_ return setUpModule, SlapOSInstanceTestCase_
def checkSoftware(slap, software_url): def checkSoftware(slap, software_url, shared_part_list):
# type: (StandaloneSlapOS, str) -> None # type: (StandaloneSlapOS, str, Iterable[str]) -> None
"""Check software installation. """Check software installation.
This perform a few basic static checks for common problems This perform a few basic static checks for common problems
with software installations. with software installations.
""" """
software_hash = md5digest(software_url)
software_hash = md5digest(software_url)
error_list = [] error_list = []
# Check that all components set rpath correctly and we don't have miss linking any libraries. # Check that all components set rpath correctly and we don't have miss linking any libraries.
for path in (os.path.join(slap.software_directory, # Also check that they are not linked against system libraries, except a white list of core
software_hash), slap.shared_directory): # system libraries.
if not glob.glob(os.path.join(path, '*')): system_lib_white_list = set((
# shared might be empty (when using a slapos command that does not support shared yet). 'libc',
continue 'libcrypt',
out = '' 'libdl',
'libgcc_s',
'libgomp',
'libm',
'libnsl',
'libpthread',
'libresolv',
'librt',
'libstdc++',
'libutil',
))
# we also ignore a few patterns for part that are known to be binary distributions,
# for which we generate LD_LIBRARY_PATH wrappers or we don't use directly.
ignored_file_patterns = set((
'*/parts/java-re*/*',
'*/parts/firefox-*/*',
'*/parts/nss/*',
'*/parts/chromium-*/*',
'*/parts/chromedriver*/*',
'*/node_modules/phantomjs-prebuilt/*',
'*/grafana/tools/phantomjs/*',
))
ldd_so_resolved_re = re.compile(
r'\t(?P<library_name>.*) => (?P<library_path>.*) \(0x')
ldd_already_loaded_re = re.compile(r'\t(?P<library_name>.*) \(0x')
ldd_not_found_re = re.compile(r'.*not found.*')
class DynamicLibraryNotFound(Exception):
"""Raise when ldd cannot resolve a library
"""
def getLddOutput(path):
# type: (str) -> Dict[str, str]
"""Parse ldd output on executable as `path` and returns a mapping
of library paths or None when library is not found, keyed by library so name.
Raises a `DynamicLibraryNotFound` if any dynamic library is not found.
Special entries, like VDSO ( linux-vdso.so.1 ) or ELF interpreter
( /lib64/ld-linux-x86-64.so.2 ) are ignored.
"""
libraries = {} # type: Dict[str, str]
try: try:
out = subprocess.check_output( ldd_output = subprocess.check_output(
"find . -type f -executable " ('ldd', path),
# We ignore parts that are binary distributions.
"| egrep -v /parts/java-re.*/ "
"| egrep -v /parts/firefox-.*/ "
"| egrep -v /parts/chromium-.*/ "
"| egrep -v /parts/chromedriver-.*/ "
# nss has no valid rpath. It does not seem to be a problem in our case.
"| egrep -v /parts/nss/ "
"| xargs ldd "
r"| egrep '(^\S|not found)' "
"| grep -B1 'not found'",
shell=True,
stderr=subprocess.STDOUT, stderr=subprocess.STDOUT,
cwd=path, universal_newlines=True,
) )
except subprocess.CalledProcessError as e: except subprocess.CalledProcessError as e:
# The "good case" is when grep does not match anything, but in if e.output not in ('\tnot a dynamic executable\n',):
# that case, it exists with exit code 1, so we accept this case. raise
if e.returncode != 1 or e.output: return libraries
error_list.append(e.output) if ldd_output == '\tstatically linked\n':
if out: return libraries
error_list.append(out)
not_found = []
for line in ldd_output.splitlines():
resolved_so_match = ldd_so_resolved_re.match(line)
ldd_already_loaded_match = ldd_already_loaded_re.match(line)
not_found_match = ldd_not_found_re.match(line)
if resolved_so_match:
libraries[resolved_so_match.group(
'library_name')] = resolved_so_match.group('library_path')
elif ldd_already_loaded_match:
# VDSO or ELF, ignore . See https://stackoverflow.com/a/35805410/7294664 for more about this
pass
elif not_found_match:
not_found.append(line)
else:
raise RuntimeError('Unknown ldd line %s for %s.' % (line, path))
if not_found:
not_found_text = '\n'.join(not_found)
raise DynamicLibraryNotFound(
'{path} has some not found libraries:\n{not_found_text}'.format(
**locals()))
return libraries
def checkExecutableLink(paths_to_check, valid_paths_for_libs):
# type: (Iterable[str], Iterable[str]) -> List[str]
executable_link_error_list = []
for path in paths_to_check:
for root, dirs, files in os.walk(path):
for f in files:
f = os.path.join(root, f)
if any(fnmatch.fnmatch(f, ignored_pattern)
for ignored_pattern in ignored_file_patterns):
continue
if os.access(f, os.X_OK):
try:
libs = getLddOutput(f)
except DynamicLibraryNotFound as e:
executable_link_error_list.append(str(e))
else:
for lib, lib_path in libs.items():
if lib.split('.')[0] in system_lib_white_list:
continue
# dynamically linked programs can only be linked with libraries
# present in software or in shared parts repository.
if any(lib_path.startswith(valid_path)
for valid_path in valid_paths_for_libs):
continue
executable_link_error_list.append(
'{f} uses system library {lib_path} for {lib}'.format(
**locals()))
return executable_link_error_list
paths_to_check = (
os.path.join(slap.software_directory,
software_hash), slap.shared_directory)
error_list.extend(
checkExecutableLink(
paths_to_check, paths_to_check + tuple(shared_part_list)))
# check this software is not referenced in any shared parts. # check this software is not referenced in any shared parts.
for signature_file in glob.glob(os.path.join(slap.shared_directory, '*', '*', for signature_file in glob.glob(os.path.join(slap.shared_directory, '*', '*',
...@@ -213,11 +300,19 @@ def checkSoftware(slap, software_url): ...@@ -213,11 +300,19 @@ def checkSoftware(slap, software_url):
raise RuntimeError('\n'.join(error_list)) raise RuntimeError('\n'.join(error_list))
def installSoftwareUrlList(cls, software_url_list, max_retry=2, debug=False): def installSoftwareUrlList(
# type: (Type[SlapOSInstanceTestCase], Iterable[str], int, bool) -> None cls,
software_url_list,
shared_part_list,
max_retry=2,
debug=False,
):
# type: (Type[SlapOSInstanceTestCase], Iterable[str], Iterable[str], int, bool) -> None
"""Install softwares on the current testing slapos, for use in `setUpModule`. """Install softwares on the current testing slapos, for use in `setUpModule`.
This also check softwares with `checkSoftware` This also check softwares with `checkSoftware`.
shared_part_list is the list of folders containing shared parts.
""" """
def _storeSoftwareLogSnapshot(name): def _storeSoftwareLogSnapshot(name):
for standalone_log in glob.glob(os.path.join( for standalone_log in glob.glob(os.path.join(
...@@ -236,7 +331,7 @@ def installSoftwareUrlList(cls, software_url_list, max_retry=2, debug=False): ...@@ -236,7 +331,7 @@ def installSoftwareUrlList(cls, software_url_list, max_retry=2, debug=False):
cls.slap.waitForSoftware(max_retry=max_retry, debug=debug) cls.slap.waitForSoftware(max_retry=max_retry, debug=debug)
_storeSoftwareLogSnapshot('setupModule') _storeSoftwareLogSnapshot('setupModule')
for software_url in software_url_list: for software_url in software_url_list:
checkSoftware(cls.slap, software_url) checkSoftware(cls.slap, software_url, shared_part_list)
except BaseException as e: except BaseException as e:
if not debug: if not debug:
cls.logger.exception("Error building software, removing") cls.logger.exception("Error building software, removing")
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment