Upstream version 7.36.149.0
[platform/framework/web/crosswalk.git] / src / build / android / pylib / linker / test_case.py
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
38 import logging
39 import os
40 import re
41 import time
42
43 from pylib import constants
44 from pylib.base import base_test_result
45
46
47 ResultType = 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.
72 re_status_line = re.compile(r'(BROWSER|RENDERER)_LINKER_TEST: (FAIL|SUCCESS)')
73
74 # Regular expression used to mach library load addresses in logcat.
75 re_library_address = re.compile(
76     r'(BROWSER|RENDERER)_LIBRARY_ADDRESS: (\S+) ([0-9A-Fa-f]+)')
77
78
79 def _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
111 def _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.old_interface.RunShellCommand(
116       'echo "%s" > %s' % (command_line, command_line_file))
117
118
119 def _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
151 def _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
194 class 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
205 class 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
215 def _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
240 def _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
281 class 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
352 class 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
381 class 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
467 class 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