| # Copyright 2013 The Chromium Authors. All rights reserved. | 
 | # Use of this source code is governed by a BSD-style license that can be | 
 | # found in the LICENSE file. | 
 |  | 
 | """Base class for linker-specific test cases. | 
 |  | 
 |    The custom dynamic linker can only be tested through a custom test case | 
 |    for various technical reasons: | 
 |  | 
 |      - It's an 'invisible feature', i.e. it doesn't expose a new API or | 
 |        behaviour, all it does is save RAM when loading native libraries. | 
 |  | 
 |      - Checking that it works correctly requires several things that do not | 
 |        fit the existing GTest-based and instrumentation-based tests: | 
 |  | 
 |          - Native test code needs to be run in both the browser and renderer | 
 |            process at the same time just after loading native libraries, in | 
 |            a completely asynchronous way. | 
 |  | 
 |          - Each test case requires restarting a whole new application process | 
 |            with a different command-line. | 
 |  | 
 |          - Enabling test support in the Linker code requires building a special | 
 |            APK with a flag to activate special test-only support code in the | 
 |            Linker code itself. | 
 |  | 
 |        Host-driven tests have also been tried, but since they're really | 
 |        sub-classes of instrumentation tests, they didn't work well either. | 
 |  | 
 |    To build and run the linker tests, do the following: | 
 |  | 
 |      ninja -C out/Debug chromium_linker_test_apk | 
 |      build/android/test_runner.py linker | 
 |  | 
 | """ | 
 | # pylint: disable=R0201 | 
 |  | 
 | import logging | 
 | import os | 
 | import re | 
 | import time | 
 |  | 
 | from pylib import constants | 
 | from pylib.base import base_test_result | 
 | from pylib.device import device_errors | 
 | from pylib.device import intent | 
 |  | 
 |  | 
 | ResultType = base_test_result.ResultType | 
 |  | 
 | _PACKAGE_NAME = 'org.chromium.chromium_linker_test_apk' | 
 | _ACTIVITY_NAME = '.ChromiumLinkerTestActivity' | 
 | _COMMAND_LINE_FILE = '/data/local/tmp/chromium-linker-test-command-line' | 
 |  | 
 | # Path to the Linker.java source file. | 
 | _LINKER_JAVA_SOURCE_PATH = ( | 
 |     'base/android/java/src/org/chromium/base/library_loader/Linker.java') | 
 |  | 
 | # A regular expression used to extract the browser shared RELRO configuration | 
 | # from the Java source file above. | 
 | _RE_LINKER_BROWSER_CONFIG = re.compile( | 
 |     r'.*BROWSER_SHARED_RELRO_CONFIG\s+=\s+' + | 
 |         r'BROWSER_SHARED_RELRO_CONFIG_(\S+)\s*;.*', | 
 |     re.MULTILINE | re.DOTALL) | 
 |  | 
 | # Logcat filters used during each test. Only the 'chromium' one is really | 
 | # needed, but the logs are added to the TestResult in case of error, and | 
 | # it is handy to have the 'chromium_android_linker' ones as well when | 
 | # troubleshooting. | 
 | _LOGCAT_FILTERS = ['*:s', 'chromium:v', 'chromium_android_linker:v'] | 
 | #_LOGCAT_FILTERS = ['*:v']  ## DEBUG | 
 |  | 
 | # Regular expression used to match status lines in logcat. | 
 | _RE_BROWSER_STATUS_LINE = re.compile(r' BROWSER_LINKER_TEST: (FAIL|SUCCESS)$') | 
 | _RE_RENDERER_STATUS_LINE = re.compile(r' RENDERER_LINKER_TEST: (FAIL|SUCCESS)$') | 
 |  | 
 | # Regular expression used to mach library load addresses in logcat. | 
 | _RE_LIBRARY_ADDRESS = re.compile( | 
 |     r'(BROWSER|RENDERER)_LIBRARY_ADDRESS: (\S+) ([0-9A-Fa-f]+)') | 
 |  | 
 |  | 
 | def _GetBrowserSharedRelroConfig(): | 
 |   """Returns a string corresponding to the Linker's configuration of shared | 
 |      RELRO sections in the browser process. This parses the Java linker source | 
 |      file to get the appropriate information. | 
 |   Return: | 
 |       None in case of error (e.g. could not locate the source file). | 
 |      'NEVER' if the browser process shall never use shared RELROs. | 
 |      'LOW_RAM_ONLY' if if uses it only on low-end devices. | 
 |      'ALWAYS' if it always uses a shared RELRO. | 
 |   """ | 
 |   source_path = \ | 
 |       os.path.join(constants.DIR_SOURCE_ROOT, _LINKER_JAVA_SOURCE_PATH) | 
 |   if not os.path.exists(source_path): | 
 |     logging.error('Could not find linker source file: ' + source_path) | 
 |     return None | 
 |  | 
 |   with open(source_path) as f: | 
 |     configs = _RE_LINKER_BROWSER_CONFIG.findall(f.read()) | 
 |     if not configs: | 
 |       logging.error( | 
 |           'Can\'t find browser shared RELRO configuration value in ' + \ | 
 |           source_path) | 
 |       return None | 
 |  | 
 |     if configs[0] not in ['NEVER', 'LOW_RAM_ONLY', 'ALWAYS']: | 
 |       logging.error('Unexpected browser config value: ' + configs[0]) | 
 |       return None | 
 |  | 
 |     logging.info('Found linker browser shared RELRO config: ' + configs[0]) | 
 |     return configs[0] | 
 |  | 
 |  | 
 | def _StartActivityAndWaitForLinkerTestStatus(device, timeout): | 
 |   """Force-start an activity and wait up to |timeout| seconds until the full | 
 |      linker test status lines appear in the logcat, recorded through |device|. | 
 |   Args: | 
 |     device: A DeviceUtils instance. | 
 |     timeout: Timeout in seconds | 
 |   Returns: | 
 |     A (status, logs) tuple, where status is a ResultType constant, and logs | 
 |     if the final logcat output as a string. | 
 |   """ | 
 |  | 
 |   # 1. Start recording logcat with appropriate filters. | 
 |   with device.GetLogcatMonitor(filter_specs=_LOGCAT_FILTERS) as logmon: | 
 |  | 
 |     # 2. Force-start activity. | 
 |     device.StartActivity( | 
 |         intent.Intent(package=_PACKAGE_NAME, activity=_ACTIVITY_NAME), | 
 |         force_stop=True) | 
 |  | 
 |     # 3. Wait up to |timeout| seconds until the test status is in the logcat. | 
 |     result = ResultType.PASS | 
 |     try: | 
 |       browser_match = logmon.WaitFor(_RE_BROWSER_STATUS_LINE, timeout=timeout) | 
 |       logging.debug('Found browser match: %s', browser_match.group(0)) | 
 |       renderer_match = logmon.WaitFor(_RE_RENDERER_STATUS_LINE, | 
 |                                       timeout=timeout) | 
 |       logging.debug('Found renderer match: %s', renderer_match.group(0)) | 
 |       if (browser_match.group(1) != 'SUCCESS' | 
 |           or renderer_match.group(1) != 'SUCCESS'): | 
 |         result = ResultType.FAIL | 
 |     except device_errors.CommandTimeoutError: | 
 |       result = ResultType.TIMEOUT | 
 |  | 
 |     return result, '\n'.join(device.adb.Logcat(dump=True)) | 
 |  | 
 |  | 
 | class LibraryLoadMap(dict): | 
 |   """A helper class to pretty-print a map of library names to load addresses.""" | 
 |   def __str__(self): | 
 |     items = ['\'%s\': 0x%x' % (name, address) for \ | 
 |         (name, address) in self.iteritems()] | 
 |     return '{%s}' % (', '.join(items)) | 
 |  | 
 |   def __repr__(self): | 
 |     return 'LibraryLoadMap(%s)' % self.__str__() | 
 |  | 
 |  | 
 | class AddressList(list): | 
 |   """A helper class to pretty-print a list of load addresses.""" | 
 |   def __str__(self): | 
 |     items = ['0x%x' % address for address in self] | 
 |     return '[%s]' % (', '.join(items)) | 
 |  | 
 |   def __repr__(self): | 
 |     return 'AddressList(%s)' % self.__str__() | 
 |  | 
 |  | 
 | def _ExtractLibraryLoadAddressesFromLogcat(logs): | 
 |   """Extract the names and addresses of shared libraries loaded in the | 
 |      browser and renderer processes. | 
 |   Args: | 
 |     logs: A string containing logcat output. | 
 |   Returns: | 
 |     A tuple (browser_libs, renderer_libs), where each item is a map of | 
 |     library names (strings) to library load addresses (ints), for the | 
 |     browser and renderer processes, respectively. | 
 |   """ | 
 |   browser_libs = LibraryLoadMap() | 
 |   renderer_libs = LibraryLoadMap() | 
 |   for m in _RE_LIBRARY_ADDRESS.finditer(logs): | 
 |     process_type, lib_name, lib_address = m.groups() | 
 |     lib_address = int(lib_address, 16) | 
 |     if process_type == 'BROWSER': | 
 |       browser_libs[lib_name] = lib_address | 
 |     elif process_type == 'RENDERER': | 
 |       renderer_libs[lib_name] = lib_address | 
 |     else: | 
 |       assert False, 'Invalid process type' | 
 |  | 
 |   return browser_libs, renderer_libs | 
 |  | 
 |  | 
 | def _CheckLoadAddressRandomization(lib_map_list, process_type): | 
 |   """Check that a map of library load addresses is random enough. | 
 |   Args: | 
 |     lib_map_list: a list of dictionaries that map library names (string) | 
 |       to load addresses (int). Each item in the list corresponds to a | 
 |       different run / process start. | 
 |     process_type: a string describing the process type. | 
 |   Returns: | 
 |     (status, logs) tuple, where <status> is True iff the load addresses are | 
 |     randomized, False otherwise, and <logs> is a string containing an error | 
 |     message detailing the libraries that are not randomized properly. | 
 |   """ | 
 |   # Collect, for each library, its list of load addresses. | 
 |   lib_addr_map = {} | 
 |   for lib_map in lib_map_list: | 
 |     for lib_name, lib_address in lib_map.iteritems(): | 
 |       if lib_name not in lib_addr_map: | 
 |         lib_addr_map[lib_name] = AddressList() | 
 |       lib_addr_map[lib_name].append(lib_address) | 
 |  | 
 |   logging.info('%s library load map: %s', process_type, lib_addr_map) | 
 |  | 
 |   # For each library, check the randomness of its load addresses. | 
 |   bad_libs = {} | 
 |   for lib_name, lib_address_list in lib_addr_map.iteritems(): | 
 |     # If all addresses are different, skip to next item. | 
 |     lib_address_set = set(lib_address_list) | 
 |     # Consider that if there is more than one pair of identical addresses in | 
 |     # the list, then randomization is broken. | 
 |     if len(lib_address_set) < len(lib_address_list) - 1: | 
 |       bad_libs[lib_name] = lib_address_list | 
 |  | 
 |  | 
 |   if bad_libs: | 
 |     return False, '%s libraries failed randomization: %s' % \ | 
 |         (process_type, bad_libs) | 
 |  | 
 |   return True, '%s libraries properly randomized: %s' % \ | 
 |       (process_type, lib_addr_map) | 
 |  | 
 |  | 
 | class LinkerTestCaseBase(object): | 
 |   """Base class for linker test cases.""" | 
 |  | 
 |   def __init__(self, is_low_memory=False): | 
 |     """Create a test case. | 
 |     Args: | 
 |       is_low_memory: True to simulate a low-memory device, False otherwise. | 
 |     """ | 
 |     self.is_low_memory = is_low_memory | 
 |     if is_low_memory: | 
 |       test_suffix = 'ForLowMemoryDevice' | 
 |     else: | 
 |       test_suffix = 'ForRegularDevice' | 
 |     class_name = self.__class__.__name__ | 
 |     self.qualified_name = '%s.%s' % (class_name, test_suffix) | 
 |     self.tagged_name = self.qualified_name | 
 |  | 
 |   def _RunTest(self, _device): | 
 |     """Run the test, must be overriden. | 
 |     Args: | 
 |       _device: A DeviceUtils interface. | 
 |     Returns: | 
 |       A (status, log) tuple, where <status> is a ResultType constant, and <log> | 
 |       is the logcat output captured during the test in case of error, or None | 
 |       in case of success. | 
 |     """ | 
 |     return ResultType.FAIL, 'Unimplemented _RunTest() method!' | 
 |  | 
 |   def Run(self, device): | 
 |     """Run the test on a given device. | 
 |     Args: | 
 |       device: Name of target device where to run the test. | 
 |     Returns: | 
 |       A base_test_result.TestRunResult() instance. | 
 |     """ | 
 |     margin = 8 | 
 |     print '[ %-*s ] %s' % (margin, 'RUN', self.tagged_name) | 
 |     logging.info('Running linker test: %s', self.tagged_name) | 
 |  | 
 |     # Create command-line file on device. | 
 |     command_line_flags = '' | 
 |     if self.is_low_memory: | 
 |       command_line_flags = '--low-memory-device' | 
 |     device.WriteFile(_COMMAND_LINE_FILE, command_line_flags) | 
 |  | 
 |     # Run the test. | 
 |     status, logs = self._RunTest(device) | 
 |  | 
 |     result_text = 'OK' | 
 |     if status == ResultType.FAIL: | 
 |       result_text = 'FAILED' | 
 |     elif status == ResultType.TIMEOUT: | 
 |       result_text = 'TIMEOUT' | 
 |     print '[ %*s ] %s' % (margin, result_text, self.tagged_name) | 
 |  | 
 |     results = base_test_result.TestRunResults() | 
 |     results.AddResult( | 
 |         base_test_result.BaseTestResult( | 
 |             self.tagged_name, | 
 |             status, | 
 |             log=logs)) | 
 |  | 
 |     return results | 
 |  | 
 |   def __str__(self): | 
 |     return self.tagged_name | 
 |  | 
 |   def __repr__(self): | 
 |     return self.tagged_name | 
 |  | 
 |  | 
 | class LinkerSharedRelroTest(LinkerTestCaseBase): | 
 |   """A linker test case to check the status of shared RELRO sections. | 
 |  | 
 |     The core of the checks performed here are pretty simple: | 
 |  | 
 |       - Clear the logcat and start recording with an appropriate set of filters. | 
 |       - Create the command-line appropriate for the test-case. | 
 |       - Start the activity (always forcing a cold start). | 
 |       - Every second, look at the current content of the filtered logcat lines | 
 |         and look for instances of the following: | 
 |  | 
 |             BROWSER_LINKER_TEST: <status> | 
 |             RENDERER_LINKER_TEST: <status> | 
 |  | 
 |         where <status> can be either FAIL or SUCCESS. These lines can appear | 
 |         in any order in the logcat. Once both browser and renderer status are | 
 |         found, stop the loop. Otherwise timeout after 30 seconds. | 
 |  | 
 |         Note that there can be other lines beginning with BROWSER_LINKER_TEST: | 
 |         and RENDERER_LINKER_TEST:, but are not followed by a <status> code. | 
 |  | 
 |       - The test case passes if the <status> for both the browser and renderer | 
 |         process are SUCCESS. Otherwise its a fail. | 
 |   """ | 
 |   def _RunTest(self, device): | 
 |     # Wait up to 30 seconds until the linker test status is in the logcat. | 
 |     return _StartActivityAndWaitForLinkerTestStatus(device, timeout=30) | 
 |  | 
 |  | 
 | class LinkerLibraryAddressTest(LinkerTestCaseBase): | 
 |   """A test case that verifies library load addresses. | 
 |  | 
 |      The point of this check is to ensure that the libraries are loaded | 
 |      according to the following rules: | 
 |  | 
 |      - For low-memory devices, they should always be loaded at the same address | 
 |        in both browser and renderer processes, both below 0x4000_0000. | 
 |  | 
 |      - For regular devices, the browser process should load libraries above | 
 |        0x4000_0000, and renderer ones below it. | 
 |   """ | 
 |   def _RunTest(self, device): | 
 |     result, logs = _StartActivityAndWaitForLinkerTestStatus(device, timeout=30) | 
 |  | 
 |     # Return immediately in case of timeout. | 
 |     if result == ResultType.TIMEOUT: | 
 |       return result, logs | 
 |  | 
 |     # Collect the library load addresses in the browser and renderer processes. | 
 |     browser_libs, renderer_libs = _ExtractLibraryLoadAddressesFromLogcat(logs) | 
 |  | 
 |     logging.info('Browser libraries: %s', browser_libs) | 
 |     logging.info('Renderer libraries: %s', renderer_libs) | 
 |  | 
 |     # Check that the same libraries are loaded into both processes: | 
 |     browser_set = set(browser_libs.keys()) | 
 |     renderer_set = set(renderer_libs.keys()) | 
 |     if browser_set != renderer_set: | 
 |       logging.error('Library set mistmach browser=%s renderer=%s', | 
 |           browser_libs.keys(), renderer_libs.keys()) | 
 |       return ResultType.FAIL, logs | 
 |  | 
 |     # And that there are not empty. | 
 |     if not browser_set: | 
 |       logging.error('No libraries loaded in any process!') | 
 |       return ResultType.FAIL, logs | 
 |  | 
 |     # Check that the renderer libraries are loaded at 'low-addresses'. i.e. | 
 |     # below 0x4000_0000, for every kind of device. | 
 |     memory_boundary = 0x40000000 | 
 |     bad_libs = [] | 
 |     for lib_name, lib_address in renderer_libs.iteritems(): | 
 |       if lib_address >= memory_boundary: | 
 |         bad_libs.append((lib_name, lib_address)) | 
 |  | 
 |     if bad_libs: | 
 |       logging.error('Renderer libraries loaded at high addresses: %s', bad_libs) | 
 |       return ResultType.FAIL, logs | 
 |  | 
 |     browser_config = _GetBrowserSharedRelroConfig() | 
 |     if not browser_config: | 
 |       return ResultType.FAIL, 'Bad linker source configuration' | 
 |  | 
 |     if browser_config == 'ALWAYS' or \ | 
 |         (browser_config == 'LOW_RAM_ONLY' and self.is_low_memory): | 
 |       # The libraries must all be loaded at the same addresses. This also | 
 |       # implicitly checks that the browser libraries are at low addresses. | 
 |       addr_mismatches = [] | 
 |       for lib_name, lib_address in browser_libs.iteritems(): | 
 |         lib_address2 = renderer_libs[lib_name] | 
 |         if lib_address != lib_address2: | 
 |           addr_mismatches.append((lib_name, lib_address, lib_address2)) | 
 |  | 
 |       if addr_mismatches: | 
 |         logging.error('Library load address mismatches: %s', | 
 |             addr_mismatches) | 
 |         return ResultType.FAIL, logs | 
 |  | 
 |     # Otherwise, check that libraries are loaded at 'high-addresses'. | 
 |     # Note that for low-memory devices, the previous checks ensure that they | 
 |     # were loaded at low-addresses. | 
 |     else: | 
 |       bad_libs = [] | 
 |       for lib_name, lib_address in browser_libs.iteritems(): | 
 |         if lib_address < memory_boundary: | 
 |           bad_libs.append((lib_name, lib_address)) | 
 |  | 
 |       if bad_libs: | 
 |         logging.error('Browser libraries loaded at low addresses: %s', bad_libs) | 
 |         return ResultType.FAIL, logs | 
 |  | 
 |     # Everything's ok. | 
 |     return ResultType.PASS, logs | 
 |  | 
 |  | 
 | class LinkerRandomizationTest(LinkerTestCaseBase): | 
 |   """A linker test case to check that library load address randomization works | 
 |      properly between successive starts of the test program/activity. | 
 |  | 
 |      This starts the activity several time (each time forcing a new process | 
 |      creation) and compares the load addresses of the libraries in them to | 
 |      detect that they have changed. | 
 |  | 
 |      In theory, two successive runs could (very rarely) use the same load | 
 |      address, so loop 5 times and compare the values there. It is assumed | 
 |      that if there are more than one pair of identical addresses, then the | 
 |      load addresses are not random enough for this test. | 
 |   """ | 
 |   def _RunTest(self, device): | 
 |     max_loops = 5 | 
 |     browser_lib_map_list = [] | 
 |     renderer_lib_map_list = [] | 
 |     logs_list = [] | 
 |     for _ in range(max_loops): | 
 |       # Start the activity. | 
 |       result, logs = _StartActivityAndWaitForLinkerTestStatus( | 
 |           device, timeout=30) | 
 |       if result == ResultType.TIMEOUT: | 
 |         # Something bad happened. Return immediately. | 
 |         return result, logs | 
 |  | 
 |       # Collect library addresses. | 
 |       browser_libs, renderer_libs = _ExtractLibraryLoadAddressesFromLogcat(logs) | 
 |       browser_lib_map_list.append(browser_libs) | 
 |       renderer_lib_map_list.append(renderer_libs) | 
 |       logs_list.append(logs) | 
 |  | 
 |     # Check randomization in the browser libraries. | 
 |     logs = '\n'.join(logs_list) | 
 |  | 
 |     browser_status, browser_logs = _CheckLoadAddressRandomization( | 
 |         browser_lib_map_list, 'Browser') | 
 |  | 
 |     renderer_status, renderer_logs = _CheckLoadAddressRandomization( | 
 |         renderer_lib_map_list, 'Renderer') | 
 |  | 
 |     browser_config = _GetBrowserSharedRelroConfig() | 
 |     if not browser_config: | 
 |       return ResultType.FAIL, 'Bad linker source configuration' | 
 |  | 
 |     if not browser_status: | 
 |       if browser_config == 'ALWAYS' or \ | 
 |           (browser_config == 'LOW_RAM_ONLY' and self.is_low_memory): | 
 |         return ResultType.FAIL, browser_logs | 
 |  | 
 |       # IMPORTANT NOTE: The system's ASLR implementation seems to be very poor | 
 |       # when starting an activity process in a loop with "adb shell am start". | 
 |       # | 
 |       # When simulating a regular device, loading libraries in the browser | 
 |       # process uses a simple mmap(NULL, ...) to let the kernel device where to | 
 |       # load the file (this is similar to what System.loadLibrary() does). | 
 |       # | 
 |       # Unfortunately, at least in the context of this test, doing so while | 
 |       # restarting the activity with the activity manager very, very, often | 
 |       # results in the system using the same load address for all 5 runs, or | 
 |       # sometimes only 4 out of 5. | 
 |       # | 
 |       # This has been tested experimentally on both Android 4.1.2 and 4.3. | 
 |       # | 
 |       # Note that this behaviour doesn't seem to happen when starting an | 
 |       # application 'normally', i.e. when using the application launcher to | 
 |       # start the activity. | 
 |       logging.info('Ignoring system\'s low randomization of browser libraries' + | 
 |                    ' for regular devices') | 
 |  | 
 |     if not renderer_status: | 
 |       return ResultType.FAIL, renderer_logs | 
 |  | 
 |     return ResultType.PASS, logs |