# Copyright (C) 2018, 2020, 2021 Igalia S.L. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # 1. Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # 2. Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR # ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. import logging from pathlib import PurePath import os import shutil import subprocess _log = logging.getLogger(__name__) class SharedObjectResolver(): def __init__(self, ldd): self._ldd = ldd if shutil.which('patchelf') is None: _log.error("Could not find `patchelf` in $PATH") raise Exception("Missing required binary `patchelf`") def _run_cmd_and_get_output(self, command): _log.debug("EXEC %s" % command) command_process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, encoding='utf-8') stdout, stderr = command_process.communicate() return command_process.returncode, stdout, stderr def _get_interpreter_objname(self, object): # Note: we use patchelf to get the object name (not the path!) # of the interpreter because this works regardless of the # architecture of the ELF file. retcode, stdout, stderr = self._run_cmd_and_get_output(['patchelf', '--print-interpreter', object]) if retcode != 0: _log.debug("patchelf stdout:\n%s\nPatchelf stderr:\n%s" % (stdout, stderr)) if 'cannot find section' in stdout: # This is fine; we only expect an interpreter in the main binary. return None raise RuntimeError('The patchelf command returned non-zero status for object %s' % object) interpreter_path = PurePath(stdout.strip()) return interpreter_path.name def _get_libs_and_interpreter(self, object): interpreter = None retcode, stdout, stderr = self._run_cmd_and_get_output([self._ldd, object]) _log.debug("ldd stdout:\n%s" % stdout) if retcode != 0: raise RuntimeError('The %s command returned non-zero status for object %s' % (self._ldd, object)) libs = [] for line in stdout.splitlines(): line = line.strip() if '=>' in line: line = line.split('=>')[1].strip() if 'not found' in line: raise RuntimeError('ldd can not resolve all dependencies for object %s.' % object) line = line.split(' ')[0].strip() if os.path.isfile(line): libs.append(line) else: line = line.split(' ')[0].strip() if os.path.isfile(line): interpreter = line if interpreter is None: # This is the case for non-native binaries. For those, we # can use a cross-ldd (xldd), but then the interpreter # looks like any other shared object in the output of # ldd. Try to identify it by looking at the object name # from the interpreter string. interpreter_objname = self._get_interpreter_objname(object) for lib in libs: if PurePath(lib).name == interpreter_objname: interpreter = lib break # If we found an interpreter, remove it from the libs. libs = [lib for lib in libs if lib != interpreter] return libs, interpreter def _ldd_recursive_get_libs_and_interpreter(self, object, already_checked_libs=[]): libs, interpreter = self._get_libs_and_interpreter(object) if libs: for lib in libs: if lib in already_checked_libs: continue # avoid recursion loops (libfreetype.so.6 <-> libharfbuzz.so.0) already_checked_libs.append(lib) sub_libs, sub_interpreter = self._ldd_recursive_get_libs_and_interpreter(lib, already_checked_libs) libs.extend(sub_libs) if sub_interpreter and interpreter and sub_interpreter != interpreter: raise RuntimeError('library %s has interpreter %s but object %s has interpreter %s' % (lib, sub_interpreter, object, interpreter)) return list(set(libs)), interpreter def get_libs_and_interpreter(self, object, already_checked_libs=[]): return self._ldd_recursive_get_libs_and_interpreter(object, already_checked_libs)