diff options
Diffstat (limited to 'dependency/vts_vndk_dependency_test.py')
-rw-r--r-- | dependency/vts_vndk_dependency_test.py | 441 |
1 files changed, 441 insertions, 0 deletions
diff --git a/dependency/vts_vndk_dependency_test.py b/dependency/vts_vndk_dependency_test.py new file mode 100644 index 0000000..6541a80 --- /dev/null +++ b/dependency/vts_vndk_dependency_test.py @@ -0,0 +1,441 @@ +#!/usr/bin/env python +# +# Copyright (C) 2020 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. +# + +# TODO(b/147454897): Keep the test logic in sync with VtsVndkDependency.py +# until it is removed. +import collections +import logging +import os +import re +import unittest + +from vts.testcases.vndk import utils +from vts.testcases.vndk.golden import vndk_data +from vts.utils.python.library import elf_parser +from vts.utils.python.vndk import vndk_utils + + +class VtsVndkDependencyTest(unittest.TestCase): + """A test case to verify vendor library dependency. + + Attributes: + _dut: The AndroidDevice under test. + _ll_ndk: Set of strings. The names of low-level NDK libraries in + /system/lib[64]. + _sp_hal: List of patterns. The names of the same-process HAL libraries + expected to be in /vendor/lib[64]. + _vndk: Set of strings. The names of VNDK-core libraries. + _vndk_sp: Set of strings. The names of VNDK-SP libraries. + _SP_HAL_LINK_PATHS: Format strings of same-process HAL's link paths. + _VENDOR_LINK_PATHS: Format strings of vendor processes' link paths. + """ + _TARGET_DIR_SEP = "/" + _TARGET_ODM_DIR = "/odm" + _TARGET_VENDOR_DIR = "/vendor" + + _SP_HAL_LINK_PATHS = [ + "/odm/{LIB}/egl", "/odm/{LIB}/hw", "/odm/{LIB}", + "/vendor/{LIB}/egl", "/vendor/{LIB}/hw", "/vendor/{LIB}" + ] + _VENDOR_LINK_PATHS = [ + "/odm/{LIB}/hw", "/odm/{LIB}/egl", "/odm/{LIB}", + "/vendor/{LIB}/hw", "/vendor/{LIB}/egl", "/vendor/{LIB}" + ] + _DEFAULT_PROGRAM_INTERPRETERS = [ + "/system/bin/linker", "/system/bin/linker64" + ] + + class ElfObject(object): + """Contains dependencies of an ELF file on target device. + + Attributes: + target_path: String. The path to the ELF file on target. + name: String. File name of the ELF. + target_dir: String. The directory containing the ELF file on + target. + bitness: Integer. Bitness of the ELF. + deps: List of strings. The names of the depended libraries. + runpaths: List of strings. The library search paths. + """ + + def __init__(self, target_path, bitness, deps, runpaths): + self.target_path = target_path + self.name = os.path.basename(target_path) + self.target_dir = os.path.dirname(target_path) + self.bitness = bitness + self.deps = deps + # Format runpaths + self.runpaths = [] + lib_dir_name = "lib64" if bitness == 64 else "lib" + for runpath in runpaths: + path = runpath.replace("${LIB}", lib_dir_name) + path = path.replace("$LIB", lib_dir_name) + path = path.replace("${ORIGIN}", self.target_dir) + path = path.replace("$ORIGIN", self.target_dir) + self.runpaths.append(path) + + def setUp(self): + """Initializes device, temporary directory, and VNDK lists.""" + self._dut = utils.AndroidDevice() + self.assertTrue(self._dut.IsRoot(), "This test requires adb root.") + + vndk_lists = vndk_data.LoadVndkLibraryListsFromResources( + self._dut.GetVndkVersion(), + vndk_data.SP_HAL, + vndk_data.LL_NDK, + vndk_data.VNDK, + vndk_data.VNDK_SP) + self.assertTrue(vndk_lists, "Cannot load VNDK library lists.") + + sp_hal_strings = vndk_lists[0] + self._sp_hal = [re.compile(x) for x in sp_hal_strings] + (self._ll_ndk, self._vndk, self._vndk_sp) = vndk_lists[1:] + + logging.debug("LL_NDK: %s", self._ll_ndk) + logging.debug("SP_HAL: %s", sp_hal_strings) + logging.debug("VNDK: %s", self._vndk) + logging.debug("VNDK_SP: %s", self._vndk_sp) + + def _IsElfObjectForAp(self, elf, target_path, abi_list): + """Checks whether an ELF object is for application processor. + + Args: + elf: The object of elf_parser.ElfParser. + target_path: The path to the ELF file on target. + abi_list: A list of strings, the ABIs of the application processor. + + Returns: + A boolean, whether the ELF object is for application processor. + """ + if not any(elf.MatchCpuAbi(x) for x in abi_list): + logging.debug("%s does not match the ABI", target_path) + return False + + # b/115567177 Skip an ELF file if it meets the following 3 conditions: + # The ELF type is executable. + if not elf.IsExecutable(): + return True + + # It requires special program interpreter. + interp = elf.GetProgramInterpreter() + if not interp or interp in self._DEFAULT_PROGRAM_INTERPRETERS: + return True + + # It does not have execute permission in the file system. + if self._dut.IsExecutable(target_path): + return True + + return False + + def _IsElfObjectBuiltForAndroid(self, elf, target_path): + """Checks whether an ELF object is built for Android. + + Some ELF objects in vendor partition require special program + interpreters. Such executable files have .interp sections, but shared + libraries don't. As there is no reliable way to identify those + libraries. This method checks .note.android.ident section which is + created by Android build system. + + Args: + elf: The object of elf_parser.ElfParser. + target_path: The path to the ELF file on target. + + Returns: + A boolean, whether the ELF object is built for Android. + """ + # b/133399940 Skip an ELF file if it does not have .note.android.ident + # section and meets one of the following conditions: + if elf.HasAndroidIdent(): + return True + + # It's in the specific directory and is a shared library. + if (target_path.startswith("/vendor/arib/lib/") and + ".so" in target_path and + elf.IsSharedObject()): + return False + + # It's in the specific directory, requires special program interpreter, + # and is executable. + if target_path.startswith("/vendor/arib/bin/"): + interp = elf.GetProgramInterpreter() + if interp and interp not in self._DEFAULT_PROGRAM_INTERPRETERS: + if elf.IsExecutable() or self._dut.IsExecutable(target_path): + return False + + return True + + @staticmethod + def _IterateFiles(host_dir): + """Iterates files in a host directory. + + Args: + host_dir: The host directory. + + Yields: + The file paths under the directory. + """ + for root_dir, dir_names, file_names in os.walk(host_dir): + for file_name in file_names: + yield os.path.join(root_dir, file_name) + + def _LoadElfObjects(self, target_dir, abi_list, elf_error_handler): + """Scans a host directory recursively and loads all ELF files in it. + + Args: + target_dir: The host directory to scan. + abi_list: A list of strings, the ABIs of the ELF files to load. + elf_error_handler: A function that takes 2 arguments + (path, exception). It is called when + the parser fails to read an ELF file. + + Returns: + List of ElfObject. + """ + objs = [] + for target_path in self._IterateFiles(target_dir): + try: + elf = elf_parser.ElfParser(target_path) + except elf_parser.ElfError: + logging.debug("%s is not an ELF file", target_path) + continue + try: + if not self._IsElfObjectForAp(elf, target_path, abi_list): + logging.info("%s is not for application processor", + target_path) + continue + if not self._IsElfObjectBuiltForAndroid(elf, target_path): + logging.warning("%s is not built for Android, which is no " + "longer exempted.", target_path) + + deps, runpaths = elf.ListDependencies() + except elf_parser.ElfError as e: + elf_error_handler(target_path, e) + continue + finally: + elf.Close() + + logging.info("%s depends on: %s", target_path, ", ".join(deps)) + if runpaths: + logging.info("%s has runpaths: %s", + target_path, ":".join(runpaths)) + objs.append(self.ElfObject(target_path, elf.bitness, deps, + runpaths)) + return objs + + def _FindLibsInLinkPaths(self, bitness, link_paths, objs): + """Finds libraries in link paths. + + Args: + bitness: 32 or 64, the bitness of the returned libraries. + link_paths: List of strings, the default link paths. + objs: List of ElfObject, the libraries/executables to be filtered + by bitness and path. + + Returns: + A defaultdict, {dir: {name: obj}} where obj is an ElfObject, dir + is obj.target_dir, and name is obj.name. + """ + namespace = collections.defaultdict(dict) + for obj in objs: + if (obj.bitness == bitness and + any(obj.target_path.startswith(link_path + + self._TARGET_DIR_SEP) + for link_path in link_paths)): + namespace[obj.target_dir][obj.name] = obj + return namespace + + def _DfsDependencies(self, lib, searched, namespace, link_paths): + """Depth-first-search for library dependencies. + + Args: + lib: ElfObject, the library to search for dependencies. + searched: The set of searched libraries. + namespace: Defaultdict, {dir: {name: obj}} containing all + searchable libraries. + link_paths: List of strings, the default link paths. + """ + if lib in searched: + return + searched.add(lib) + for dep_name in lib.deps: + for link_path in lib.runpaths + link_paths: + if dep_name in namespace[link_path]: + self._DfsDependencies(namespace[link_path][dep_name], + searched, namespace, link_paths) + break + + def _FindDisallowedDependencies(self, objs, namespace, link_paths, + *vndk_lists): + """Tests if libraries/executables have disallowed dependencies. + + Args: + objs: Collection of ElfObject, the libraries/executables under + test. + namespace: Defaultdict, {dir: {name: obj}} containing all libraries + in the linker namespace. + link_paths: List of strings, the default link paths. + vndk_lists: Collections of library names in VNDK, VNDK-SP, etc. + + Returns: + List of tuples (path, disallowed_dependencies). + """ + dep_errors = [] + for obj in objs: + disallowed_libs = [] + for dep_name in obj.deps: + if any((dep_name in vndk_list) for vndk_list in vndk_lists): + continue + if any((dep_name in namespace[link_path]) for link_path in + obj.runpaths + link_paths): + continue + disallowed_libs.append(dep_name) + + if disallowed_libs: + dep_errors.append((obj.target_path, disallowed_libs)) + return dep_errors + + def _TestElfDependency(self, bitness, objs): + """Tests vendor libraries/executables and SP-HAL dependencies. + + Args: + bitness: 32 or 64, the bitness of the vendor libraries. + objs: List of ElfObject. The libraries/executables in odm and + vendor partitions. + + Returns: + List of tuples (path, disallowed_dependencies). + """ + vndk_sp_ext_dirs = vndk_utils.GetVndkSpExtDirectories(bitness) + + vendor_link_paths = [vndk_utils.FormatVndkPath(x, bitness) for + x in self._VENDOR_LINK_PATHS] + vendor_namespace = self._FindLibsInLinkPaths(bitness, + vendor_link_paths, objs) + # Exclude VNDK and VNDK-SP extensions from vendor libraries. + for vndk_ext_dir in (vndk_utils.GetVndkExtDirectories(bitness) + + vndk_utils.GetVndkSpExtDirectories(bitness)): + vendor_namespace.pop(vndk_ext_dir, None) + logging.info("%d-bit odm, vendor, and SP-HAL libraries:", bitness) + for dir_path, libs in vendor_namespace.items(): + logging.info("%s: %s", dir_path, ",".join(libs.keys())) + + sp_hal_link_paths = [vndk_utils.FormatVndkPath(x, bitness) for + x in self._SP_HAL_LINK_PATHS] + sp_hal_namespace = self._FindLibsInLinkPaths(bitness, + sp_hal_link_paths, objs) + + # Find same-process HAL and dependencies + sp_hal_libs = set() + for link_path in sp_hal_link_paths: + for obj in sp_hal_namespace[link_path].values(): + if any(x.match(obj.target_path) for x in self._sp_hal): + self._DfsDependencies(obj, sp_hal_libs, sp_hal_namespace, + sp_hal_link_paths) + logging.info("%d-bit SP-HAL libraries: %s", + bitness, ", ".join(x.name for x in sp_hal_libs)) + + # Find VNDK-SP extension libraries and their dependencies. + vndk_sp_ext_libs = set(obj for obj in objs if + obj.bitness == bitness and + obj.target_dir in vndk_sp_ext_dirs) + vndk_sp_ext_deps = set() + for lib in vndk_sp_ext_libs: + self._DfsDependencies(lib, vndk_sp_ext_deps, sp_hal_namespace, + sp_hal_link_paths) + logging.info("%d-bit VNDK-SP extension libraries and dependencies: %s", + bitness, ", ".join(x.name for x in vndk_sp_ext_deps)) + + # A vendor library/executable is allowed to depend on + # LL-NDK + # VNDK + # VNDK-SP + # Other libraries in vendor link paths + vendor_objs = {obj for obj in objs if + obj.bitness == bitness and + obj not in sp_hal_libs and + obj not in vndk_sp_ext_deps} + dep_errors = self._FindDisallowedDependencies( + vendor_objs, vendor_namespace, vendor_link_paths, + self._ll_ndk, self._vndk, self._vndk_sp) + + # A VNDK-SP extension library/dependency is allowed to depend on + # LL-NDK + # VNDK-SP + # Libraries in vendor link paths + # Other VNDK-SP extension libraries, which is a subset of VNDK-SP + # + # However, it is not allowed to indirectly depend on VNDK. i.e., the + # depended vendor libraries must not depend on VNDK. + # + # vndk_sp_ext_deps and sp_hal_libs may overlap. Their dependency + # restrictions are the same. + dep_errors.extend(self._FindDisallowedDependencies( + vndk_sp_ext_deps - sp_hal_libs, vendor_namespace, + vendor_link_paths, self._ll_ndk, self._vndk_sp)) + + if not vndk_utils.IsVndkRuntimeEnforced(self._dut): + logging.warning("Ignore dependency errors: %s", dep_errors) + dep_errors = [] + + # A same-process HAL library is allowed to depend on + # LL-NDK + # VNDK-SP + # Other same-process HAL libraries and dependencies + dep_errors.extend(self._FindDisallowedDependencies( + sp_hal_libs, sp_hal_namespace, sp_hal_link_paths, + self._ll_ndk, self._vndk_sp)) + return dep_errors + + def testElfDependency(self): + """Tests vendor libraries/executables and SP-HAL dependencies.""" + read_errors = [] + abi_list = self._dut.GetCpuAbiList() + objs = [] + for target_dir in (self._TARGET_ODM_DIR, self._TARGET_VENDOR_DIR): + if self._dut.IsDirectory(target_dir): + objs.extend(self._LoadElfObjects( + target_dir, abi_list, + lambda p, e: read_errors.append((p, str(e))))) + + dep_errors = self._TestElfDependency(32, objs) + if self._dut.GetCpuAbiList(64): + dep_errors.extend(self._TestElfDependency(64, objs)) + + assert_lines = [] + if read_errors: + error_lines = ["%s: %s" % (x[0], x[1]) for x in read_errors] + logging.error("%d read errors:\n%s", + len(read_errors), "\n".join(error_lines)) + assert_lines.extend(error_lines[:20]) + + if dep_errors: + error_lines = ["%s: %s" % (x[0], ", ".join(x[1])) + for x in dep_errors] + logging.error("%d disallowed dependencies:\n%s", + len(dep_errors), "\n".join(error_lines)) + assert_lines.extend(error_lines[:20]) + + error_count = len(read_errors) + len(dep_errors) + if error_count: + if error_count > len(assert_lines): + assert_lines.append("...") + assert_lines.append("Total number of errors: " + str(error_count)) + self.fail("\n".join(assert_lines)) + + +if __name__ == "__main__": + unittest.main() |