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