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.
5 """Base class for linker-specific test cases.
7 The custom dynamic linker can only be tested through a custom test case
8 for various technical reasons:
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.
13 - Checking that it works correctly requires several things that do not
14 fit the existing GTest-based and instrumentation-based tests:
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.
20 - Each test case requires restarting a whole new application process
21 with a different command-line.
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
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.
30 To build and run the linker tests, do the following:
32 ninja -C out/Debug content_linker_test_apk
33 build/android/test_runner.py linker
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
50 ResultType = base_test_result.ResultType
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'
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'
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)
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
71 _LOGCAT_FILTERS = [ '*:s', 'chromium:v', 'content_android_linker:v' ]
72 #_LOGCAT_FILTERS = [ '*:v' ] ## DEBUG
74 # Regular expression used to match status lines in logcat.
75 re_status_line = re.compile(r'(BROWSER|RENDERER)_LINKER_TEST: (FAIL|SUCCESS)')
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]+)')
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.
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.
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)
98 with open(source_path) as f:
99 configs = _RE_LINKER_BROWSER_CONFIG.findall(f.read())
102 'Can\'t find browser shared RELRO configuration value in ' + \
106 if configs[0] not in ['NEVER', 'LOW_RAM_ONLY', 'ALWAYS']:
107 logging.error('Unexpected browser config value: ' + configs[0])
110 logging.info('Found linker browser shared RELRO config: ' + configs[0])
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))
121 def _CheckLinkerTestStatus(logcat):
122 """Parse the content of |logcat| and checks for both a browser and
123 renderer status line.
126 logcat: A string to parse. Can include line separators.
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.
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':
139 browser_success = (status == 'SUCCESS')
140 elif process_type == 'RENDERER':
141 renderer_found = True
142 renderer_success = (status == 'SUCCESS')
144 assert False, 'Invalid process type ' + process_type
146 if browser_found and renderer_found:
147 return (True, browser_success, renderer_success)
149 # Didn't find anything.
150 return (False, None, None)
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|.
157 adb: An AndroidCommands instance. This assumes adb.StartRecordingLogcat()
158 was called previously.
159 timeout: Timeout in seconds.
161 ResultType.TIMEOUT in case of timeout, ResulType.PASS if both status lines
162 report 'SUCCESS', or ResulType.FAIL otherwise.
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|.
170 adb: An AndroidCommands instance.
171 timeout: Timeout in seconds
173 A (status, logs) tuple, where status is a ResultType constant, and logs
174 if the final logcat output as a string.
176 # 1. Start recording logcat with appropriate filters.
177 adb.StartRecordingLogcat(clear=True, filters=_LOGCAT_FILTERS)
180 # 2. Force-start activity.
181 adb.StartActivity(package=_PACKAGE_NAME,
182 activity=_ACTIVITY_NAME,
185 # 3. Wait up to |timeout| seconds until the test status is in the logcat.
189 while num_tries < max_tries:
192 found, browser_ok, renderer_ok = _CheckLinkerTestStatus(
193 adb.GetCurrentRecordedLogcat())
198 logs = adb.StopRecordingLogcat()
200 if num_tries >= max_tries:
201 return ResultType.TIMEOUT, logs
203 if browser_ok and renderer_ok:
204 return ResultType.PASS, logs
206 return ResultType.FAIL, logs
209 class LibraryLoadMap(dict):
210 """A helper class to pretty-print a map of library names to load addresses."""
212 items = ['\'%s\': 0x%x' % (name, address) for \
213 (name, address) in self.iteritems()]
214 return '{%s}' % (', '.join(items))
217 return 'LibraryLoadMap(%s)' % self.__str__()
220 class AddressList(list):
221 """A helper class to pretty-print a list of load addresses."""
223 items = ['0x%x' % address for address in self]
224 return '[%s]' % (', '.join(items))
227 return 'AddressList(%s)' % self.__str__()
230 def _ExtractLibraryLoadAddressesFromLogcat(logs):
231 """Extract the names and addresses of shared libraries loaded in the
232 browser and renderer processes.
234 logs: A string containing logcat output.
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.
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
250 assert False, 'Invalid process type'
252 return browser_libs, renderer_libs
255 def _CheckLoadAddressRandomization(lib_map_list, process_type):
256 """Check that a map of library load addresses is random enough.
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.
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.
267 # Collect, for each library, its list of load addresses.
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)
275 logging.info('%s library load map: %s', process_type, lib_addr_map)
277 # For each library, check the randomness of its load addresses.
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
290 return False, '%s libraries failed randomization: %s' % \
291 (process_type, bad_libs)
293 return True, '%s libraries properly randomized: %s' % \
294 (process_type, lib_addr_map)
297 class LinkerTestCaseBase(object):
298 """Base class for linker test cases."""
300 def __init__(self, is_low_memory=False):
301 """Create a test case.
303 is_low_memory: True to simulate a low-memory device, False otherwise.
305 self.is_low_memory = is_low_memory
307 test_suffix = 'ForLowMemoryDevice'
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
314 def _RunTest(self, adb):
315 """Run the test, must be overriden.
317 adb: An AndroidCommands instance to the device.
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
323 return ResultType.FAIL, 'Unimplemented _RunTest() method!'
325 def Run(self, device):
326 """Run the test on a given device.
328 device: Name of target device where to run the test.
330 A base_test_result.TestRunResult() instance.
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)
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)
344 status, logs = self._RunTest(adb)
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)
353 results = base_test_result.TestRunResults()
355 base_test_result.BaseTestResult(
363 return self.tagged_name
366 return self.tagged_name
369 class LinkerSharedRelroTest(LinkerTestCaseBase):
370 """A linker test case to check the status of shared RELRO sections.
372 The core of the checks performed here are pretty simple:
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:
380 BROWSER_LINKER_TEST: <status>
381 RENDERER_LINKER_TEST: <status>
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.
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.
390 - The test case passes if the <status> for both the browser and renderer
391 process are SUCCESS. Otherwise its a fail.
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)
398 class LinkerLibraryAddressTest(LinkerTestCaseBase):
399 """A test case that verifies library load addresses.
401 The point of this check is to ensure that the libraries are loaded
402 according to the following rules:
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.
407 - For regular devices, the browser process should load libraries above
408 0x4000_0000, and renderer ones below it.
410 def _RunTest(self, adb):
411 result, logs = _StartActivityAndWaitForLinkerTestStatus(adb, timeout=30)
413 # Return immediately in case of timeout.
414 if result == ResultType.TIMEOUT:
417 # Collect the library load addresses in the browser and renderer processes.
418 browser_libs, renderer_libs = _ExtractLibraryLoadAddressesFromLogcat(logs)
420 logging.info('Browser libraries: %s', browser_libs)
421 logging.info('Renderer libraries: %s', renderer_libs)
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
431 # And that there are not empty.
433 logging.error('No libraries loaded in any process!')
434 return ResultType.FAIL, logs
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
440 for lib_name, lib_address in renderer_libs.iteritems():
441 if lib_address >= memory_boundary:
442 bad_libs.append((lib_name, lib_address))
445 logging.error('Renderer libraries loaded at high addresses: %s', bad_libs)
446 return ResultType.FAIL, logs
448 browser_config = _GetBrowserSharedRelroConfig()
449 if not browser_config:
450 return ResultType.FAIL, 'Bad linker source configuration'
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.
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))
463 logging.error('Library load address mismatches: %s',
465 return ResultType.FAIL, logs
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.
472 for lib_name, lib_address in browser_libs.iteritems():
473 if lib_address < memory_boundary:
474 bad_libs.append((lib_name, lib_address))
477 logging.error('Browser libraries loaded at low addresses: %s', bad_libs)
478 return ResultType.FAIL, logs
481 return ResultType.PASS, logs
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.
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.
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.
497 def _RunTest(self, adb):
499 browser_lib_map_list = []
500 renderer_lib_map_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.
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)
515 # Check randomization in the browser libraries.
516 logs = '\n'.join(logs_list)
518 browser_status, browser_logs = _CheckLoadAddressRandomization(
519 browser_lib_map_list, 'Browser')
521 renderer_status, renderer_logs = _CheckLoadAddressRandomization(
522 renderer_lib_map_list, 'Renderer')
524 browser_config = _GetBrowserSharedRelroConfig()
525 if not browser_config:
526 return ResultType.FAIL, 'Bad linker source configuration'
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
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".
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).
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.
545 # This has been tested experimentally on both Android 4.1.2 and 4.3.
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')
553 if not renderer_status:
554 return ResultType.FAIL, renderer_logs
556 return ResultType.PASS, logs
559 class LinkerLowMemoryThresholdTest(LinkerTestCaseBase):
560 """This test checks that the definitions for the low-memory device physical
561 RAM threshold are identical in the base/ and linker sources. Because these
562 two components should absolutely not depend on each other, it's difficult
563 to perform this check correctly at runtime inside the linker test binary
564 without introducing hairy dependency issues in the build, or complicated
567 To work-around this, this test looks directly into the sources for a
568 definition of the same constant that should look like:
570 #define ANDROID_LOW_MEMORY_DEVICE_THRESHOLD_MB <number>
572 And will check that the values for <number> are identical in all of
575 # A regular expression used to find the definition of the threshold in all
577 _RE_THRESHOLD_DEFINITION = re.compile(
578 r'^\s*#\s*define\s+ANDROID_LOW_MEMORY_DEVICE_THRESHOLD_MB\s+(\d+)\s*$',
581 # The list of source files, relative to DIR_SOURCE_ROOT, which must contain
582 # a line that matches the re above.
584 'base/android/sys_utils.cc',
585 'content/common/android/linker/linker_jni.cc' ]
587 def _RunTest(self, adb):
590 # First, collect all the values in all input sources.
591 re = LinkerLowMemoryThresholdTest._RE_THRESHOLD_DEFINITION
592 for source in LinkerLowMemoryThresholdTest._SOURCES_LIST:
593 source_path = os.path.join(constants.DIR_SOURCE_ROOT, source);
594 if not os.path.exists(source_path):
595 logging.error('Missing source file: ' + source_path)
598 with open(source_path) as f:
599 source_text = f.read()
600 # For some reason, re.match() never works here.
601 source_values = re.findall(source_text)
602 if not source_values:
603 logging.error('Missing low-memory threshold definition in ' + \
605 logging.error('Source:\n%s\n' % source_text)
608 values += source_values
610 # Second, check that they are all the same.
612 for value in values[1:]:
613 if value != values[0]:
614 logging.error('Value mismatch: ' + repr(values))
618 return ResultType.FAIL, 'Incorrect low-end memory threshold definitions!'
620 return ResultType.PASS, ''