Home | History | Annotate | Download | only in linker
      1 # Copyright 2013 The Chromium Authors. All rights reserved.
      2 # Use of this source code is governed by a BSD-style license that can be
      3 # found in the LICENSE file.
      4 
      5 """Base class for linker-specific test cases.
      6 
      7    The custom dynamic linker can only be tested through a custom test case
      8    for various technical reasons:
      9 
     10      - It's an 'invisible feature', i.e. it doesn't expose a new API or
     11        behaviour, all it does is save RAM when loading native libraries.
     12 
     13      - Checking that it works correctly requires several things that do not
     14        fit the existing GTest-based and instrumentation-based tests:
     15 
     16          - Native test code needs to be run in both the browser and renderer
     17            process at the same time just after loading native libraries, in
     18            a completely asynchronous way.
     19 
     20          - Each test case requires restarting a whole new application process
     21            with a different command-line.
     22 
     23          - Enabling test support in the Linker code requires building a special
     24            APK with a flag to activate special test-only support code in the
     25            Linker code itself.
     26 
     27        Host-driven tests have also been tried, but since they're really
     28        sub-classes of instrumentation tests, they didn't work well either.
     29 
     30    To build and run the linker tests, do the following:
     31 
     32      ninja -C out/Debug content_linker_test_apk
     33      build/android/test_runner.py linker
     34 
     35 """
     36 
     37 import logging
     38 import os
     39 import re
     40 import StringIO
     41 import subprocess
     42 import tempfile
     43 import time
     44 
     45 from pylib import constants
     46 from pylib import android_commands
     47 from pylib import flag_changer
     48 from pylib.base import base_test_result
     49 
     50 ResultType = base_test_result.ResultType
     51 
     52 _PACKAGE_NAME='org.chromium.content_linker_test_apk'
     53 _ACTIVITY_NAME='.ContentLinkerTestActivity'
     54 _COMMAND_LINE_FILE='/data/local/tmp/content-linker-test-command-line'
     55 
     56 # Path to the Linker.java source file.
     57 _LINKER_JAVA_SOURCE_PATH = \
     58     'content/public/android/java/src/org/chromium/content/app/Linker.java'
     59 
     60 # A regular expression used to extract the browser shared RELRO configuration
     61 # from the Java source file above.
     62 _RE_LINKER_BROWSER_CONFIG = \
     63     re.compile(r'.*BROWSER_SHARED_RELRO_CONFIG\s+=\s+' + \
     64                'BROWSER_SHARED_RELRO_CONFIG_(\S+)\s*;.*',
     65                re.MULTILINE | re.DOTALL)
     66 
     67 # Logcat filters used during each test. Only the 'chromium' one is really
     68 # needed, but the logs are added to the TestResult in case of error, and
     69 # it is handy to have the 'content_android_linker' ones as well when
     70 # troubleshooting.
     71 _LOGCAT_FILTERS = [ '*:s', 'chromium:v', 'content_android_linker:v' ]
     72 #_LOGCAT_FILTERS = [ '*:v' ]  ## DEBUG
     73 
     74 # Regular expression used to match status lines in logcat.
     75 re_status_line = re.compile(r'(BROWSER|RENDERER)_LINKER_TEST: (FAIL|SUCCESS)')
     76 
     77 # Regular expression used to mach library load addresses in logcat.
     78 re_library_address = re.compile(
     79     r'(BROWSER|RENDERER)_LIBRARY_ADDRESS: (\S+) ([0-9A-Fa-f]+)')
     80 
     81 
     82 def _GetBrowserSharedRelroConfig():
     83   """Returns a string corresponding to the Linker's configuration of shared
     84      RELRO sections in the browser process. This parses the Java linker source
     85      file to get the appropriate information.
     86   Return:
     87       None in case of error (e.g. could not locate the source file).
     88      'NEVER' if the browser process shall never use shared RELROs.
     89      'LOW_RAM_ONLY' if if uses it only on low-end devices.
     90      'ALWAYS' if it always uses a shared RELRO.
     91   """
     92   source_path = \
     93       os.path.join(constants.DIR_SOURCE_ROOT, _LINKER_JAVA_SOURCE_PATH)
     94   if not os.path.exists(source_path):
     95     logging.error('Could not find linker source file: ' + source_path)
     96     return None
     97 
     98   with open(source_path) as f:
     99     configs = _RE_LINKER_BROWSER_CONFIG.findall(f.read())
    100     if not configs:
    101       logging.error(
    102           'Can\'t find browser shared RELRO configuration value in ' + \
    103           source_path)
    104       return None
    105 
    106     if configs[0] not in ['NEVER', 'LOW_RAM_ONLY', 'ALWAYS']:
    107       logging.error('Unexpected browser config value: ' + configs[0])
    108       return None
    109 
    110     logging.info('Found linker browser shared RELRO config: ' + configs[0])
    111     return configs[0]
    112 
    113 
    114 def _WriteCommandLineFile(adb, command_line, command_line_file):
    115   """Create a command-line file on the device. This does not use FlagChanger
    116      because its implementation assumes the device has 'su', and thus does
    117      not work at all with production devices."""
    118   adb.RunShellCommand('echo "%s" > %s' % (command_line, command_line_file))
    119 
    120 
    121 def _CheckLinkerTestStatus(logcat):
    122   """Parse the content of |logcat| and checks for both a browser and
    123      renderer status line.
    124 
    125   Args:
    126     logcat: A string to parse. Can include line separators.
    127 
    128   Returns:
    129     A tuple, result[0] is True if there is a complete match, then
    130     result[1] and result[2] will be True or False to reflect the
    131     test status for the browser and renderer processes, respectively.
    132   """
    133   browser_found = False
    134   renderer_found = False
    135   for m in re_status_line.finditer(logcat):
    136     process_type, status = m.groups()
    137     if process_type == 'BROWSER':
    138       browser_found = True
    139       browser_success = (status == 'SUCCESS')
    140     elif process_type == 'RENDERER':
    141       renderer_found = True
    142       renderer_success = (status == 'SUCCESS')
    143     else:
    144       assert False, 'Invalid process type ' + process_type
    145 
    146   if browser_found and renderer_found:
    147     return (True, browser_success, renderer_success)
    148 
    149   # Didn't find anything.
    150   return (False, None, None)
    151 
    152 
    153 def _WaitForLinkerTestStatus(adb, timeout):
    154   """Wait up to |timeout| seconds until the full linker test status lines appear
    155      in the logcat being recorded with |adb|.
    156   Args:
    157     adb: An AndroidCommands instance. This assumes adb.StartRecordingLogcat()
    158          was called previously.
    159     timeout: Timeout in seconds.
    160   Returns:
    161     ResultType.TIMEOUT in case of timeout, ResulType.PASS if both status lines
    162     report 'SUCCESS', or ResulType.FAIL otherwise.
    163   """
    164 
    165 
    166 def _StartActivityAndWaitForLinkerTestStatus(adb, timeout):
    167   """Force-start an activity and wait up to |timeout| seconds until the full
    168      linker test status lines appear in the logcat, recorded through |adb|.
    169   Args:
    170     adb: An AndroidCommands instance.
    171     timeout: Timeout in seconds
    172   Returns:
    173     A (status, logs) tuple, where status is a ResultType constant, and logs
    174     if the final logcat output as a string.
    175   """
    176   # 1. Start recording logcat with appropriate filters.
    177   adb.StartRecordingLogcat(clear=True, filters=_LOGCAT_FILTERS)
    178 
    179   try:
    180     # 2. Force-start activity.
    181     adb.StartActivity(package=_PACKAGE_NAME,
    182                       activity=_ACTIVITY_NAME,
    183                       force_stop=True)
    184 
    185     # 3. Wait up to |timeout| seconds until the test status is in the logcat.
    186     num_tries = 0
    187     max_tries = timeout
    188     found = False
    189     while num_tries < max_tries:
    190       time.sleep(1)
    191       num_tries += 1
    192       found, browser_ok, renderer_ok = _CheckLinkerTestStatus(
    193           adb.GetCurrentRecordedLogcat())
    194       if found:
    195         break
    196 
    197   finally:
    198     logs = adb.StopRecordingLogcat()
    199 
    200   if num_tries >= max_tries:
    201     return ResultType.TIMEOUT, logs
    202 
    203   if browser_ok and renderer_ok:
    204     return ResultType.PASS, logs
    205 
    206   return ResultType.FAIL, logs
    207 
    208 
    209 class LibraryLoadMap(dict):
    210   """A helper class to pretty-print a map of library names to load addresses."""
    211   def __str__(self):
    212     items = ['\'%s\': 0x%x' % (name, address) for \
    213         (name, address) in self.iteritems()]
    214     return '{%s}' % (', '.join(items))
    215 
    216   def __repr__(self):
    217     return 'LibraryLoadMap(%s)' % self.__str__()
    218 
    219 
    220 class AddressList(list):
    221   """A helper class to pretty-print a list of load addresses."""
    222   def __str__(self):
    223     items = ['0x%x' % address for address in self]
    224     return '[%s]' % (', '.join(items))
    225 
    226   def __repr__(self):
    227     return 'AddressList(%s)' % self.__str__()
    228 
    229 
    230 def _ExtractLibraryLoadAddressesFromLogcat(logs):
    231   """Extract the names and addresses of shared libraries loaded in the
    232      browser and renderer processes.
    233   Args:
    234     logs: A string containing logcat output.
    235   Returns:
    236     A tuple (browser_libs, renderer_libs), where each item is a map of
    237     library names (strings) to library load addresses (ints), for the
    238     browser and renderer processes, respectively.
    239   """
    240   browser_libs = LibraryLoadMap()
    241   renderer_libs = LibraryLoadMap()
    242   for m in re_library_address.finditer(logs):
    243     process_type, lib_name, lib_address = m.groups()
    244     lib_address = int(lib_address, 16)
    245     if process_type == 'BROWSER':
    246       browser_libs[lib_name] = lib_address
    247     elif process_type == 'RENDERER':
    248       renderer_libs[lib_name] = lib_address
    249     else:
    250       assert False, 'Invalid process type'
    251 
    252   return browser_libs, renderer_libs
    253 
    254 
    255 def _CheckLoadAddressRandomization(lib_map_list, process_type):
    256   """Check that a map of library load addresses is random enough.
    257   Args:
    258     lib_map_list: a list of dictionaries that map library names (string)
    259       to load addresses (int). Each item in the list corresponds to a
    260       different run / process start.
    261     process_type: a string describing the process type.
    262   Returns:
    263     (status, logs) tuple, where <status> is True iff the load addresses are
    264     randomized, False otherwise, and <logs> is a string containing an error
    265     message detailing the libraries that are not randomized properly.
    266   """
    267   # Collect, for each library, its list of load addresses.
    268   lib_addr_map = {}
    269   for lib_map in lib_map_list:
    270     for lib_name, lib_address in lib_map.iteritems():
    271       if lib_name not in lib_addr_map:
    272         lib_addr_map[lib_name] = AddressList()
    273       lib_addr_map[lib_name].append(lib_address)
    274 
    275   logging.info('%s library load map: %s', process_type, lib_addr_map)
    276 
    277   # For each library, check the randomness of its load addresses.
    278   bad_libs = {}
    279   success = True
    280   for lib_name, lib_address_list in lib_addr_map.iteritems():
    281     # If all addresses are different, skip to next item.
    282     lib_address_set = set(lib_address_list)
    283     # Consider that if there is more than one pair of identical addresses in
    284     # the list, then randomization is broken.
    285     if len(lib_address_set) < len(lib_address_list) - 1:
    286       bad_libs[lib_name] = lib_address_list
    287 
    288 
    289   if bad_libs:
    290     return False, '%s libraries failed randomization: %s' % \
    291         (process_type, bad_libs)
    292 
    293   return True, '%s libraries properly randomized: %s' % \
    294       (process_type, lib_addr_map)
    295 
    296 
    297 class LinkerTestCaseBase(object):
    298   """Base class for linker test cases."""
    299 
    300   def __init__(self, is_low_memory=False):
    301     """Create a test case.
    302     Args:
    303       is_low_memory: True to simulate a low-memory device, False otherwise.
    304     """
    305     self.is_low_memory = is_low_memory
    306     if is_low_memory:
    307         test_suffix = 'ForLowMemoryDevice'
    308     else:
    309         test_suffix = 'ForRegularDevice'
    310     class_name = self.__class__.__name__
    311     self.qualified_name = '%s.%s' % (class_name, test_suffix)
    312     self.tagged_name = self.qualified_name
    313 
    314   def _RunTest(self, adb):
    315     """Run the test, must be overriden.
    316     Args:
    317       adb: An AndroidCommands instance to the device.
    318     Returns:
    319       A (status, log) tuple, where <status> is a ResultType constant, and <log>
    320       is the logcat output captured during the test in case of error, or None
    321       in case of success.
    322     """
    323     return ResultType.FAIL, 'Unimplemented _RunTest() method!'
    324 
    325   def Run(self, device):
    326     """Run the test on a given device.
    327     Args:
    328       device: Name of target device where to run the test.
    329     Returns:
    330       A base_test_result.TestRunResult() instance.
    331     """
    332     margin = 8
    333     print '[ %-*s ] %s' % (margin, 'RUN', self.tagged_name)
    334     logging.info('Running linker test: %s', self.tagged_name)
    335     adb = android_commands.AndroidCommands(device)
    336 
    337     # Create command-line file on device.
    338     command_line_flags = ''
    339     if self.is_low_memory:
    340       command_line_flags = '--low-memory-device'
    341     _WriteCommandLineFile(adb, command_line_flags, _COMMAND_LINE_FILE)
    342 
    343     # Run the test.
    344     status, logs = self._RunTest(adb)
    345 
    346     result_text = 'OK'
    347     if status == ResultType.FAIL:
    348       result_text = 'FAILED'
    349     elif status == ResultType.TIMEOUT:
    350       result_text = 'TIMEOUT'
    351     print '[ %*s ] %s' % (margin, result_text, self.tagged_name)
    352 
    353     results = base_test_result.TestRunResults()
    354     results.AddResult(
    355         base_test_result.BaseTestResult(
    356             self.tagged_name,
    357             status,
    358             logs))
    359 
    360     return results
    361 
    362   def __str__(self):
    363     return self.tagged_name
    364 
    365   def __repr__(self):
    366     return self.tagged_name
    367 
    368 
    369 class LinkerSharedRelroTest(LinkerTestCaseBase):
    370   """A linker test case to check the status of shared RELRO sections.
    371 
    372     The core of the checks performed here are pretty simple:
    373 
    374       - Clear the logcat and start recording with an appropriate set of filters.
    375       - Create the command-line appropriate for the test-case.
    376       - Start the activity (always forcing a cold start).
    377       - Every second, look at the current content of the filtered logcat lines
    378         and look for instances of the following:
    379 
    380             BROWSER_LINKER_TEST: <status>
    381             RENDERER_LINKER_TEST: <status>
    382 
    383         where <status> can be either FAIL or SUCCESS. These lines can appear
    384         in any order in the logcat. Once both browser and renderer status are
    385         found, stop the loop. Otherwise timeout after 30 seconds.
    386 
    387         Note that there can be other lines beginning with BROWSER_LINKER_TEST:
    388         and RENDERER_LINKER_TEST:, but are not followed by a <status> code.
    389 
    390       - The test case passes if the <status> for both the browser and renderer
    391         process are SUCCESS. Otherwise its a fail.
    392   """
    393   def _RunTest(self, adb):
    394     # Wait up to 30 seconds until the linker test status is in the logcat.
    395     return _StartActivityAndWaitForLinkerTestStatus(adb, timeout=30)
    396 
    397 
    398 class LinkerLibraryAddressTest(LinkerTestCaseBase):
    399   """A test case that verifies library load addresses.
    400 
    401      The point of this check is to ensure that the libraries are loaded
    402      according to the following rules:
    403 
    404      - For low-memory devices, they should always be loaded at the same address
    405        in both browser and renderer processes, both below 0x4000_0000.
    406 
    407      - For regular devices, the browser process should load libraries above
    408        0x4000_0000, and renderer ones below it.
    409   """
    410   def _RunTest(self, adb):
    411     result, logs = _StartActivityAndWaitForLinkerTestStatus(adb, timeout=30)
    412 
    413     # Return immediately in case of timeout.
    414     if result == ResultType.TIMEOUT:
    415       return result, logs
    416 
    417     # Collect the library load addresses in the browser and renderer processes.
    418     browser_libs, renderer_libs = _ExtractLibraryLoadAddressesFromLogcat(logs)
    419 
    420     logging.info('Browser libraries: %s', browser_libs)
    421     logging.info('Renderer libraries: %s', renderer_libs)
    422 
    423     # Check that the same libraries are loaded into both processes:
    424     browser_set = set(browser_libs.keys())
    425     renderer_set = set(renderer_libs.keys())
    426     if browser_set != renderer_set:
    427       logging.error('Library set mistmach browser=%s renderer=%s',
    428           browser_libs.keys(), renderer_libs.keys())
    429       return ResultType.FAIL, logs
    430 
    431     # And that there are not empty.
    432     if not browser_set:
    433       logging.error('No libraries loaded in any process!')
    434       return ResultType.FAIL, logs
    435 
    436     # Check that the renderer libraries are loaded at 'low-addresses'. i.e.
    437     # below 0x4000_0000, for every kind of device.
    438     memory_boundary = 0x40000000
    439     bad_libs = []
    440     for lib_name, lib_address in renderer_libs.iteritems():
    441       if lib_address >= memory_boundary:
    442         bad_libs.append((lib_name, lib_address))
    443 
    444     if bad_libs:
    445       logging.error('Renderer libraries loaded at high addresses: %s', bad_libs)
    446       return ResultType.FAIL, logs
    447 
    448     browser_config = _GetBrowserSharedRelroConfig()
    449     if not browser_config:
    450       return ResultType.FAIL, 'Bad linker source configuration'
    451 
    452     if browser_config == 'ALWAYS' or \
    453         (browser_config == 'LOW_RAM_ONLY' and self.is_low_memory):
    454       # The libraries must all be loaded at the same addresses. This also
    455       # implicitly checks that the browser libraries are at low addresses.
    456       addr_mismatches = []
    457       for lib_name, lib_address in browser_libs.iteritems():
    458         lib_address2 = renderer_libs[lib_name]
    459         if lib_address != lib_address2:
    460           addr_mismatches.append((lib_name, lib_address, lib_address2))
    461 
    462       if addr_mismatches:
    463         logging.error('Library load address mismatches: %s',
    464             addr_mismatches)
    465         return ResultType.FAIL, logs
    466 
    467     # Otherwise, check that libraries are loaded at 'high-addresses'.
    468     # Note that for low-memory devices, the previous checks ensure that they
    469     # were loaded at low-addresses.
    470     else:
    471       bad_libs = []
    472       for lib_name, lib_address in browser_libs.iteritems():
    473         if lib_address < memory_boundary:
    474           bad_libs.append((lib_name, lib_address))
    475 
    476       if bad_libs:
    477         logging.error('Browser libraries loaded at low addresses: %s', bad_libs)
    478         return ResultType.FAIL, logs
    479 
    480     # Everything's ok.
    481     return ResultType.PASS, logs
    482 
    483 
    484 class LinkerRandomizationTest(LinkerTestCaseBase):
    485   """A linker test case to check that library load address randomization works
    486      properly between successive starts of the test program/activity.
    487 
    488      This starts the activity several time (each time forcing a new process
    489      creation) and compares the load addresses of the libraries in them to
    490      detect that they have changed.
    491 
    492      In theory, two successive runs could (very rarely) use the same load
    493      address, so loop 5 times and compare the values there. It is assumed
    494      that if there are more than one pair of identical addresses, then the
    495      load addresses are not random enough for this test.
    496   """
    497   def _RunTest(self, adb):
    498     max_loops = 5
    499     browser_lib_map_list = []
    500     renderer_lib_map_list = []
    501     logs_list = []
    502     for loop in range(max_loops):
    503       # Start the activity.
    504       result, logs = _StartActivityAndWaitForLinkerTestStatus(adb, timeout=30)
    505       if result == ResultType.TIMEOUT:
    506         # Something bad happened. Return immediately.
    507         return result, logs
    508 
    509       # Collect library addresses.
    510       browser_libs, renderer_libs = _ExtractLibraryLoadAddressesFromLogcat(logs)
    511       browser_lib_map_list.append(browser_libs)
    512       renderer_lib_map_list.append(renderer_libs)
    513       logs_list.append(logs)
    514 
    515     # Check randomization in the browser libraries.
    516     logs = '\n'.join(logs_list)
    517 
    518     browser_status, browser_logs = _CheckLoadAddressRandomization(
    519         browser_lib_map_list, 'Browser')
    520 
    521     renderer_status, renderer_logs = _CheckLoadAddressRandomization(
    522         renderer_lib_map_list, 'Renderer')
    523 
    524     browser_config = _GetBrowserSharedRelroConfig()
    525     if not browser_config:
    526       return ResultType.FAIL, 'Bad linker source configuration'
    527 
    528     if not browser_status:
    529       if browser_config == 'ALWAYS' or \
    530           (browser_config == 'LOW_RAM_ONLY' and self.is_low_memory):
    531         return ResultType.FAIL, browser_logs
    532 
    533       # IMPORTANT NOTE: The system's ASLR implementation seems to be very poor
    534       # when starting an activity process in a loop with "adb shell am start".
    535       #
    536       # When simulating a regular device, loading libraries in the browser
    537       # process uses a simple mmap(NULL, ...) to let the kernel device where to
    538       # load the file (this is similar to what System.loadLibrary() does).
    539       #
    540       # Unfortunately, at least in the context of this test, doing so while
    541       # restarting the activity with the activity manager very, very, often
    542       # results in the system using the same load address for all 5 runs, or
    543       # sometimes only 4 out of 5.
    544       #
    545       # This has been tested experimentally on both Android 4.1.2 and 4.3.
    546       #
    547       # Note that this behaviour doesn't seem to happen when starting an
    548       # application 'normally', i.e. when using the application launcher to
    549       # start the activity.
    550       logging.info('Ignoring system\'s low randomization of browser libraries' +
    551                    ' for regular devices')
    552 
    553     if not renderer_status:
    554       return ResultType.FAIL, renderer_logs
    555 
    556     return ResultType.PASS, logs
    557