diff --git a/core/check_elf_file.mk b/core/check_elf_file.mk new file mode 100644 index 0000000000..69d891cfdb --- /dev/null +++ b/core/check_elf_file.mk @@ -0,0 +1,45 @@ +# Check the correctness of the prebuilt ELF files +# +# This check ensures that DT_SONAME matches with the filename, DT_NEEDED +# matches the shared libraries specified in LOCAL_SHARED_LIBRARIES, and all +# undefined symbols in the prebuilt binary can be found in one of the shared +# libraries specified in LOCAL_SHARED_LIBRARIES. +# +# Inputs: +# - LOCAL_ALLOW_UNDEFINED_SYMBOLS +# - LOCAL_BUILT_MODULE +# - LOCAL_IS_HOST_MODULE +# - LOCAL_MODULE_CLASS +# - intermediates +# - my_installed_module_stem +# - my_prebuilt_src_file + +ifndef LOCAL_IS_HOST_MODULE +ifneq ($(filter $(LOCAL_MODULE_CLASS),SHARED_LIBRARIES EXECUTABLES NATIVE_TESTS),) +check_elf_files_stamp := $(intermediates)/check_elf_files.timestamp +$(check_elf_files_stamp): PRIVATE_SONAME := $(if $(filter $(LOCAL_MODULE_CLASS),SHARED_LIBRARIES),$(my_installed_module_stem)) +$(check_elf_files_stamp): PRIVATE_ALLOW_UNDEFINED_SYMBOLS := $(LOCAL_ALLOW_UNDEFINED_SYMBOLS) +$(check_elf_files_stamp): PRIVATE_SHARED_LIBRARY_FILES := # This variable will be set by `core/main.mk` +$(check_elf_files_stamp): $(my_prebuilt_src_file) $(CHECK_ELF_FILE) $(LLVM_READOBJ) + @echo Check prebuilt ELF binary: $< + $(hide) mkdir -p $(dir $@) + $(hide) rm -f $@ + $(hide) $(CHECK_ELF_FILE) \ + --skip-bad-elf-magic \ + --skip-unknown-elf-machine \ + $(if $(PRIVATE_SONAME),--soname $(PRIVATE_SONAME)) \ + $(foreach l,$(PRIVATE_SHARED_LIBRARY_FILES),--shared-lib $(l)) \ + $(if $(PRIVATE_ALLOW_UNDEFINED_SYMBOLS),--allow-undefined-symbols) \ + --llvm-readobj=$(LLVM_READOBJ) \ + $< + $(hide) touch $@ + +ifneq ($(PRODUCT_CHECK_ELF_FILES)$(CHECK_ELF_FILES),) +ifneq ($(LOCAL_CHECK_ELF_FILES),false) +$(LOCAL_BUILT_MODULE): $(check_elf_files_stamp) +check-elf-files: $(check_elf_files_stamp) +endif # LOCAL_CHECK_ELF_FILES +endif # PRODUCT_CHECK_ELF_FILES or CHECK_ELF_FILES + +endif # SHARED_LIBRARIES, EXECUTABLES, NATIVE_TESTS +endif # !LOCAL_IS_HOST_MODULE diff --git a/core/clang/config.mk b/core/clang/config.mk index ca3a1fae3c..063fd3473d 100644 --- a/core/clang/config.mk +++ b/core/clang/config.mk @@ -1,5 +1,7 @@ ## Clang configurations. +LLVM_READOBJ := $(LLVM_PREBUILTS_BASE)/$(BUILD_OS)-x86/$(LLVM_PREBUILTS_VERSION)/bin/llvm-readobj + LLVM_RTLIB_PATH := $(LLVM_PREBUILTS_BASE)/linux-x86/$(LLVM_PREBUILTS_VERSION)/lib64/clang/$(LLVM_RELEASE_VERSION)/lib/linux/ define convert-to-clang-flags diff --git a/core/clear_vars.mk b/core/clear_vars.mk index f9e07f89e4..e19091633e 100644 --- a/core/clear_vars.mk +++ b/core/clear_vars.mk @@ -304,6 +304,7 @@ LOCAL_WARNINGS_ENABLE:= LOCAL_WHOLE_STATIC_LIBRARIES:= LOCAL_XOM:= LOCAL_YACCFLAGS:= +LOCAL_CHECK_ELF_FILES:= # TODO: deprecate, it does nothing OVERRIDE_BUILT_MODULE_PATH:= diff --git a/core/config.mk b/core/config.mk index 89bcdb26cd..cc8b40b939 100644 --- a/core/config.mk +++ b/core/config.mk @@ -707,6 +707,7 @@ JARJAR := $(HOST_OUT_JAVA_LIBRARIES)/jarjar.jar DATA_BINDING_COMPILER := $(HOST_OUT_JAVA_LIBRARIES)/databinding-compiler.jar FAT16COPY := build/make/tools/fat16copy.py CHECK_LINK_TYPE := build/make/tools/check_link_type.py +CHECK_ELF_FILE := build/make/tools/check_elf_file.py LPMAKE := $(HOST_OUT_EXECUTABLES)/lpmake$(HOST_EXECUTABLE_SUFFIX) BUILD_SUPER_IMAGE := build/make/tools/releasetools/build_super_image.py diff --git a/core/main.mk b/core/main.mk index c84cbe0676..bac613cc59 100644 --- a/core/main.mk +++ b/core/main.mk @@ -786,9 +786,43 @@ ifdef HOST_CROSS_OS $(call resolve-shared-libs-depes,HOST_CROSS_,,true) endif +# Pass the shared libraries dependencies to prebuilt ELF file check. +define add-elf-file-check-shared-lib +$(1): PRIVATE_SHARED_LIBRARY_FILES += $(2) +$(1): $(2) +endef + +define resolve-shared-libs-for-elf-file-check +$(foreach m,$($(if $(2),$($(1)2ND_ARCH_VAR_PREFIX))$(1)DEPENDENCIES_ON_SHARED_LIBRARIES),\ + $(eval p := $(subst :,$(space),$(m)))\ + $(eval mod := $(firstword $(p)))\ + \ + $(eval deps := $(subst $(comma),$(space),$(lastword $(p))))\ + $(if $(2),$(eval deps := $(addsuffix $($(1)2ND_ARCH_MODULE_SUFFIX),$(deps))))\ + $(eval root := $(1)OUT$(if $(call streq,$(1),TARGET_),_ROOT))\ + $(eval deps := $(filter $($(root))/%$($(1)SHLIB_SUFFIX),$(call module-built-files,$(deps))))\ + \ + $(eval r := $(firstword $(filter \ + $($(if $(2),$($(1)2ND_ARCH_VAR_PREFIX))TARGET_OUT_INTERMEDIATES)/EXECUTABLES/%\ + $($(if $(2),$($(1)2ND_ARCH_VAR_PREFIX))TARGET_OUT_INTERMEDIATES)/NATIVE_TESTS/%\ + $($(if $(2),$($(1)2ND_ARCH_VAR_PREFIX))TARGET_OUT_INTERMEDIATES)/SHARED_LIBRARIES/%,\ + $(call module-built-files,$(mod)))))\ + \ + $(if $(r),\ + $(eval stamp := $(dir $(r))check_elf_files.timestamp)\ + $(eval $(call add-elf-file-check-shared-lib,$(stamp),$(deps)))\ + )) +endef + +$(call resolve-shared-libs-for-elf-file-check,TARGET_) +ifdef TARGET_2ND_ARCH +$(call resolve-shared-libs-for-elf-file-check,TARGET_,true) +endif + m := r := p := +stamp := deps := add-required-deps := @@ -1529,6 +1563,9 @@ findbugs: $(INTERNAL_FINDBUGS_HTML_TARGET) $(INTERNAL_FINDBUGS_XML_TARGET) .PHONY: findlsdumps findlsdumps: $(FIND_LSDUMPS_FILE) +.PHONY: check-elf-files +check-elf-files: + #xxx scrape this from ALL_MODULE_NAME_TAGS .PHONY: modules modules: diff --git a/core/prebuilt_internal.mk b/core/prebuilt_internal.mk index 9b995e28a7..0b48a77331 100644 --- a/core/prebuilt_internal.mk +++ b/core/prebuilt_internal.mk @@ -168,6 +168,9 @@ my_common := include $(BUILD_SYSTEM)/link_type.mk endif # prebuilt_module_is_a_library +# Check prebuilt ELF binaries. +include $(BUILD_SYSTEM)/check_elf_file.mk + # The real dependency will be added after all Android.mks are loaded and the install paths # of the shared libraries are determined. ifdef LOCAL_INSTALLED_MODULE diff --git a/core/product.mk b/core/product.mk index 0219ce4b45..c3cbb0c782 100644 --- a/core/product.mk +++ b/core/product.mk @@ -227,6 +227,7 @@ _product_var_list := \ PRODUCT_BUILD_USERDATA_IMAGE \ PRODUCT_UPDATABLE_BOOT_MODULES \ PRODUCT_UPDATABLE_BOOT_LOCATIONS \ + PRODUCT_CHECK_ELF_FILES \ define dump-product $(info ==== $(1) ====)\ diff --git a/core/product_config.mk b/core/product_config.mk index 27e8ecf093..1f44692ecd 100644 --- a/core/product_config.mk +++ b/core/product_config.mk @@ -477,6 +477,11 @@ PRODUCT_ENFORCE_RRO_EXCLUDED_OVERLAYS := \ PRODUCT_ENFORCE_RRO_TARGETS := \ $(strip $(PRODUCTS.$(INTERNAL_PRODUCT).PRODUCT_ENFORCE_RRO_TARGETS)) +# Whether the product would like to check prebuilt ELF files. +PRODUCT_CHECK_ELF_FILES := \ + $(strip $(PRODUCTS.$(INTERNAL_PRODUCT).PRODUCT_CHECK_ELF_FILES)) +.KATI_READONLY := PRODUCT_CHECK_ELF_FILES + # Add reserved headroom to a system image. PRODUCT_SYSTEM_HEADROOM := \ $(strip $(PRODUCTS.$(INTERNAL_PRODUCT).PRODUCT_SYSTEM_HEADROOM)) diff --git a/core/soong_cc_prebuilt.mk b/core/soong_cc_prebuilt.mk index 088b076440..39cc0f0afa 100644 --- a/core/soong_cc_prebuilt.mk +++ b/core/soong_cc_prebuilt.mk @@ -93,6 +93,12 @@ ifdef LOCAL_USE_VNDK name_without_suffix := endif +# Check prebuilt ELF binaries. +ifneq ($(LOCAL_CHECK_ELF_FILES),) +my_prebuilt_src_file := $(LOCAL_PREBUILT_MODULE_FILE) +include $(BUILD_SYSTEM)/check_elf_file.mk +endif + # The real dependency will be added after all Android.mks are loaded and the install paths # of the shared libraries are determined. ifdef LOCAL_INSTALLED_MODULE diff --git a/core/soong_config.mk b/core/soong_config.mk index 1b6b9d3f4e..f8088b7209 100644 --- a/core/soong_config.mk +++ b/core/soong_config.mk @@ -123,6 +123,8 @@ $(call add_json_bool, Product_is_iot, $(filter true,$(PRODUCT $(call add_json_bool, Treble_linker_namespaces, $(filter true,$(PRODUCT_TREBLE_LINKER_NAMESPACES))) $(call add_json_bool, Enforce_vintf_manifest, $(filter true,$(PRODUCT_ENFORCE_VINTF_MANIFEST))) +$(call add_json_bool, Check_elf_files, $(filter true,$(PRODUCT_CHECK_ELF_FILES))) + $(call add_json_bool, Uml, $(filter true,$(TARGET_USER_MODE_LINUX))) $(call add_json_bool, Use_lmkd_stats_log, $(filter true,$(TARGET_LMKD_STATS_LOG))) $(call add_json_str, VendorPath, $(TARGET_COPY_OUT_VENDOR)) diff --git a/tools/check_elf_file.py b/tools/check_elf_file.py new file mode 100755 index 0000000000..38c1cf4264 --- /dev/null +++ b/tools/check_elf_file.py @@ -0,0 +1,538 @@ +#!/usr/bin/env python +# +# Copyright (C) 2019 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""ELF file checker. + +This command ensures all undefined symbols in an ELF file can be resolved to +global (or weak) symbols defined in shared objects specified in DT_NEEDED +entries. +""" + +from __future__ import print_function + +import argparse +import collections +import os +import os.path +import re +import struct +import subprocess +import sys + + +_ELF_MAGIC = b'\x7fELF' + + +# Known machines +_EM_386 = 3 +_EM_ARM = 40 +_EM_X86_64 = 62 +_EM_AARCH64 = 183 + +_KNOWN_MACHINES = {_EM_386, _EM_ARM, _EM_X86_64, _EM_AARCH64} + + +# ELF header struct +_ELF_HEADER_STRUCT = ( + ('ei_magic', '4s'), + ('ei_class', 'B'), + ('ei_data', 'B'), + ('ei_version', 'B'), + ('ei_osabi', 'B'), + ('ei_pad', '8s'), + ('e_type', 'H'), + ('e_machine', 'H'), + ('e_version', 'I'), +) + +_ELF_HEADER_STRUCT_FMT = ''.join(_fmt for _, _fmt in _ELF_HEADER_STRUCT) + + +ELFHeader = collections.namedtuple( + 'ELFHeader', [_name for _name, _ in _ELF_HEADER_STRUCT]) + + +ELF = collections.namedtuple( + 'ELF', + ('dt_soname', 'dt_needed', 'imported', 'exported', 'header')) + + +def _get_os_name(): + """Get the host OS name.""" + if sys.platform == 'linux2': + return 'linux' + if sys.platform == 'darwin': + return 'darwin' + raise ValueError(sys.platform + ' is not supported') + + +def _get_build_top(): + """Find the build top of the source tree ($ANDROID_BUILD_TOP).""" + prev_path = None + curr_path = os.path.abspath(os.getcwd()) + while prev_path != curr_path: + if os.path.exists(os.path.join(curr_path, '.repo')): + return curr_path + prev_path = curr_path + curr_path = os.path.dirname(curr_path) + return None + + +def _select_latest_llvm_version(versions): + """Select the latest LLVM prebuilts version from a set of versions.""" + pattern = re.compile('clang-r([0-9]+)([a-z]?)') + found_rev = 0 + found_ver = None + for curr_ver in versions: + match = pattern.match(curr_ver) + if not match: + continue + curr_rev = int(match.group(1)) + if not found_ver or curr_rev > found_rev or ( + curr_rev == found_rev and curr_ver > found_ver): + found_rev = curr_rev + found_ver = curr_ver + return found_ver + + +def _get_latest_llvm_version(llvm_dir): + """Find the latest LLVM prebuilts version from `llvm_dir`.""" + return _select_latest_llvm_version(os.listdir(llvm_dir)) + + +def _get_llvm_dir(): + """Find the path to LLVM prebuilts.""" + build_top = _get_build_top() + + llvm_prebuilts_base = os.environ.get('LLVM_PREBUILTS_BASE') + if not llvm_prebuilts_base: + llvm_prebuilts_base = os.path.join('prebuilts', 'clang', 'host') + + llvm_dir = os.path.join( + build_top, llvm_prebuilts_base, _get_os_name() + '-x86') + + if not os.path.exists(llvm_dir): + return None + + llvm_prebuilts_version = os.environ.get('LLVM_PREBUILTS_VERSION') + if not llvm_prebuilts_version: + llvm_prebuilts_version = _get_latest_llvm_version(llvm_dir) + + llvm_dir = os.path.join(llvm_dir, llvm_prebuilts_version) + + if not os.path.exists(llvm_dir): + return None + + return llvm_dir + + +def _get_llvm_readobj(): + """Find the path to llvm-readobj executable.""" + llvm_dir = _get_llvm_dir() + llvm_readobj = os.path.join(llvm_dir, 'bin', 'llvm-readobj') + return llvm_readobj if os.path.exists(llvm_readobj) else 'llvm-readobj' + + +class ELFError(ValueError): + """Generic ELF parse error""" + pass + + +class ELFInvalidMagicError(ELFError): + """Invalid ELF magic word error""" + def __init__(self): + super(ELFInvalidMagicError, self).__init__('bad ELF magic') + + +class ELFParser(object): + """ELF file parser""" + + @classmethod + def _read_elf_header(cls, elf_file_path): + """Read the ELF magic word from the beginning of the file.""" + with open(elf_file_path, 'rb') as elf_file: + buf = elf_file.read(struct.calcsize(_ELF_HEADER_STRUCT_FMT)) + try: + return ELFHeader(*struct.unpack(_ELF_HEADER_STRUCT_FMT, buf)) + except struct.error: + return None + + + @classmethod + def open(cls, elf_file_path, llvm_readobj): + """Open and parse the ELF file.""" + # Parse the ELF header for simple sanity checks. + header = cls._read_elf_header(elf_file_path) + if not header or header.ei_magic != _ELF_MAGIC: + raise ELFInvalidMagicError() + + # Run llvm-readobj and parse the output. + return cls._read_llvm_readobj(elf_file_path, header, llvm_readobj) + + + @classmethod + def _find_prefix(cls, pattern, lines_it): + """Iterate `lines_it` until finding a string that starts with `pattern`.""" + for line in lines_it: + if line.startswith(pattern): + return True + return False + + + @classmethod + def _read_llvm_readobj(cls, elf_file_path, header, llvm_readobj): + """Run llvm-readobj and parse the output.""" + proc = subprocess.Popen( + [llvm_readobj, '-dynamic-table', '-dyn-symbols', elf_file_path], + stdout=subprocess.PIPE, stderr=subprocess.PIPE) + out, _ = proc.communicate() + lines = out.splitlines() + return cls._parse_llvm_readobj(elf_file_path, header, lines) + + + @classmethod + def _parse_llvm_readobj(cls, elf_file_path, header, lines): + """Parse the output of llvm-readobj.""" + lines_it = iter(lines) + imported, exported = cls._parse_dynamic_symbols(lines_it) + dt_soname, dt_needed = cls._parse_dynamic_table(elf_file_path, lines_it) + return ELF(dt_soname, dt_needed, imported, exported, header) + + + _DYNAMIC_SECTION_START_PATTERN = 'DynamicSection [' + + _DYNAMIC_SECTION_NEEDED_PATTERN = re.compile( + '^ 0x[0-9a-fA-F]+\\s+NEEDED\\s+Shared library: \\[(.*)\\]$') + + _DYNAMIC_SECTION_SONAME_PATTERN = re.compile( + '^ 0x[0-9a-fA-F]+\\s+SONAME\\s+Library soname: \\[(.*)\\]$') + + _DYNAMIC_SECTION_END_PATTERN = ']' + + + @classmethod + def _parse_dynamic_table(cls, elf_file_path, lines_it): + """Parse the dynamic table section.""" + dt_soname = os.path.basename(elf_file_path) + dt_needed = [] + + dynamic = cls._find_prefix(cls._DYNAMIC_SECTION_START_PATTERN, lines_it) + if not dynamic: + return (dt_soname, dt_needed) + + for line in lines_it: + if line == cls._DYNAMIC_SECTION_END_PATTERN: + break + + match = cls._DYNAMIC_SECTION_NEEDED_PATTERN.match(line) + if match: + dt_needed.append(match.group(1)) + continue + + match = cls._DYNAMIC_SECTION_SONAME_PATTERN.match(line) + if match: + dt_soname = match.group(1) + continue + + return (dt_soname, dt_needed) + + + _DYNAMIC_SYMBOLS_START_PATTERN = 'DynamicSymbols [' + _DYNAMIC_SYMBOLS_END_PATTERN = ']' + + _SYMBOL_ENTRY_START_PATTERN = ' Symbol {' + _SYMBOL_ENTRY_PATTERN = re.compile('^ ([A-Za-z0-9_]+): (.*)$') + _SYMBOL_ENTRY_PAREN_PATTERN = re.compile( + '\\s+\\((?:(?:\\d+)|(?:0x[0-9a-fA-F]+))\\)$') + _SYMBOL_ENTRY_END_PATTERN = ' }' + + + @classmethod + def _parse_symbol_name(cls, name_with_version): + """Split `name_with_version` into name and version. This function may split + at last occurrence of `@@` or `@`.""" + name, version = name_with_version.rsplit('@', 1) + if name and name[-1] == '@': + name = name[:-1] + return (name, version) + + + @classmethod + def _parse_dynamic_symbols(cls, lines_it): + """Parse dynamic symbol table and collect imported and exported symbols.""" + imported = collections.defaultdict(set) + exported = collections.defaultdict(set) + + for symbol in cls._parse_dynamic_symbols_internal(lines_it): + name, version = cls._parse_symbol_name(symbol['Name']) + if name: + if symbol['Section'] == 'Undefined': + if symbol['Binding'] != 'Weak': + imported[name].add(version) + else: + if symbol['Binding'] != 'Local': + exported[name].add(version) + + # Freeze the returned imported/exported dict. + return (dict(imported), dict(exported)) + + + @classmethod + def _parse_dynamic_symbols_internal(cls, lines_it): + """Parse symbols entries and yield each symbols.""" + + if not cls._find_prefix(cls._DYNAMIC_SYMBOLS_START_PATTERN, lines_it): + return + + for line in lines_it: + if line == cls._DYNAMIC_SYMBOLS_END_PATTERN: + return + + if line == cls._SYMBOL_ENTRY_START_PATTERN: + symbol = {} + continue + + if line == cls._SYMBOL_ENTRY_END_PATTERN: + yield symbol + symbol = None + continue + + match = cls._SYMBOL_ENTRY_PATTERN.match(line) + if match: + key = match.group(1) + value = cls._SYMBOL_ENTRY_PAREN_PATTERN.sub('', match.group(2)) + symbol[key] = value + continue + + +class Checker(object): + """ELF file checker that checks DT_SONAME, DT_NEEDED, and symbols.""" + + def __init__(self, llvm_readobj): + self._file_path = '' + self._file_under_test = None + self._shared_libs = [] + + self._llvm_readobj = llvm_readobj + + + if sys.stderr.isatty(): + _ERROR_TAG = '\033[0;1;31merror:\033[m' # Red error + _NOTE_TAG = '\033[0;1;30mnote:\033[m' # Black note + else: + _ERROR_TAG = 'error:' # Red error + _NOTE_TAG = 'note:' # Black note + + + def _error(self, *args): + """Emit an error to stderr.""" + print(self._file_path + ': ' + self._ERROR_TAG, *args, file=sys.stderr) + + + def _note(self, *args): + """Emit a note to stderr.""" + print(self._file_path + ': ' + self._NOTE_TAG, *args, file=sys.stderr) + + + def _load_elf_file(self, path, skip_bad_elf_magic): + """Load an ELF file from the `path`.""" + try: + return ELFParser.open(path, self._llvm_readobj) + except (IOError, OSError): + self._error('Failed to open "{}".'.format(path)) + sys.exit(2) + except ELFInvalidMagicError: + if skip_bad_elf_magic: + sys.exit(0) + else: + self._error('File "{}" must have a valid ELF magic word.'.format(path)) + sys.exit(2) + except: + self._error('An unknown error occurred while opening "{}".'.format(path)) + raise + + + def load_file_under_test(self, path, skip_bad_elf_magic, + skip_unknown_elf_machine): + """Load file-under-test (either an executable or a shared lib).""" + self._file_path = path + self._file_under_test = self._load_elf_file(path, skip_bad_elf_magic) + + if skip_unknown_elf_machine and \ + self._file_under_test.header.e_machine not in _KNOWN_MACHINES: + sys.exit(0) + + + def load_shared_libs(self, shared_lib_paths): + """Load shared libraries.""" + for path in shared_lib_paths: + self._shared_libs.append(self._load_elf_file(path, False)) + + + def check_dt_soname(self, soname): + """Check whether DT_SONAME matches installation file name.""" + if self._file_under_test.dt_soname != soname: + self._error('DT_SONAME "{}" must be equal to the file name "{}".' + .format(self._file_under_test.dt_soname, soname)) + sys.exit(2) + + + def check_dt_needed(self): + """Check whether all DT_NEEDED entries are specified in the build + system.""" + + missing_shared_libs = False + + # Collect the DT_SONAMEs from shared libs specified in the build system. + specified_sonames = {lib.dt_soname for lib in self._shared_libs} + + # Chech whether all DT_NEEDED entries are specified. + for lib in self._file_under_test.dt_needed: + if lib not in specified_sonames: + self._error('DT_NEEDED "{}" is not specified in shared_libs.' + .format(lib.decode('utf-8'))) + missing_shared_libs = True + + if missing_shared_libs: + dt_needed = sorted(set(self._file_under_test.dt_needed)) + modules = [re.sub('\\.so$', '', lib) for lib in dt_needed] + + self._note() + self._note('Fix suggestions:') + self._note( + ' Android.bp: shared_libs: [' + + ', '.join('"' + module + '"' for module in modules) + '],') + self._note( + ' Android.mk: LOCAL_SHARED_LIBRARIES := ' + ' '.join(modules)) + + self._note() + self._note('If the fix above doesn\'t work, bypass this check with:') + self._note(' Android.bp: check_elf_files: false,') + self._note(' Android.mk: LOCAL_CHECK_ELF_FILES := false') + + sys.exit(2) + + + @staticmethod + def _find_symbol(lib, name, version): + """Check whether the symbol name and version matches a definition in + lib.""" + try: + lib_sym_vers = lib.exported[name] + except KeyError: + return False + if version == '': # Symbol version is not requested + return True + return version in lib_sym_vers + + + @classmethod + def _find_symbol_from_libs(cls, libs, name, version): + """Check whether the symbol name and version is defined in one of the + shared libraries in libs.""" + for lib in libs: + if cls._find_symbol(lib, name, version): + return lib + return None + + + def check_symbols(self): + """Check whether all undefined symbols are resolved to a definition.""" + all_elf_files = [self._file_under_test] + self._shared_libs + missing_symbols = [] + for sym, imported_vers in self._file_under_test.imported.iteritems(): + for imported_ver in imported_vers: + lib = self._find_symbol_from_libs(all_elf_files, sym, imported_ver) + if not lib: + missing_symbols.append((sym, imported_ver)) + + if missing_symbols: + for sym, ver in sorted(missing_symbols): + sym = sym.decode('utf-8') + if ver: + sym += '@' + ver.decode('utf-8') + self._error('Unresolved symbol: {}'.format(sym)) + + self._note() + self._note('Some dependencies might be changed, thus the symbol(s) ' + 'above cannot be resolved.') + self._note('Please re-build the prebuilt file: "{}".' + .format(self._file_path)) + + self._note() + self._note('If this is a new prebuilt file and it is designed to have ' + 'unresolved symbols, add one of the following properties:') + self._note(' Android.bp: allow_undefined_symbols: true,') + self._note(' Android.mk: LOCAL_ALLOW_UNDEFINED_SYMBOLS := true') + + sys.exit(2) + + +def _parse_args(): + """Parse command line options.""" + parser = argparse.ArgumentParser() + + # Input file + parser.add_argument('file', + help='Path to the input file to be checked') + parser.add_argument('--soname', + help='Shared object name of the input file') + + # Shared library dependencies + parser.add_argument('--shared-lib', action='append', default=[], + help='Path to shared library dependencies') + + # Check options + parser.add_argument('--skip-bad-elf-magic', action='store_true', + help='Ignore the input file without the ELF magic word') + parser.add_argument('--skip-unknown-elf-machine', action='store_true', + help='Ignore the input file with unknown machine ID') + parser.add_argument('--allow-undefined-symbols', action='store_true', + help='Ignore unresolved undefined symbols') + + # Other options + parser.add_argument('--llvm-readobj', + help='Path to the llvm-readobj executable') + + return parser.parse_args() + + +def main(): + """Main function""" + args = _parse_args() + + llvm_readobj = args.llvm_readobj + if not llvm_readobj: + llvm_readobj = _get_llvm_readobj() + + # Load ELF files + checker = Checker(llvm_readobj) + checker.load_file_under_test( + args.file, args.skip_bad_elf_magic, args.skip_unknown_elf_machine) + checker.load_shared_libs(args.shared_lib) + + # Run checks + if args.soname: + checker.check_dt_soname(args.soname) + + checker.check_dt_needed() + + if not args.allow_undefined_symbols: + checker.check_symbols() + + +if __name__ == '__main__': + main()