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