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 chromium_linker_test_apk
33 build/android/test_runner.py linker
36 # pylint: disable=R0201
43 from pylib import constants
44 from pylib.base import base_test_result
47 ResultType = base_test_result.ResultType
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'
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')
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)
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
68 _LOGCAT_FILTERS = [ '*:s', 'chromium:v', 'chromium_android_linker:v' ]
69 #_LOGCAT_FILTERS = [ '*:v' ] ## DEBUG
71 # Regular expression used to match status lines in logcat.
72 re_status_line = re.compile(r'(BROWSER|RENDERER)_LINKER_TEST: (FAIL|SUCCESS)')
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]+)')
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.
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.
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)
95 with open(source_path) as f:
96 configs = _RE_LINKER_BROWSER_CONFIG.findall(f.read())
99 'Can\'t find browser shared RELRO configuration value in ' + \
103 if configs[0] not in ['NEVER', 'LOW_RAM_ONLY', 'ALWAYS']:
104 logging.error('Unexpected browser config value: ' + configs[0])
107 logging.info('Found linker browser shared RELRO config: ' + configs[0])
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))
119 def _CheckLinkerTestStatus(logcat):
120 """Parse the content of |logcat| and checks for both a browser and
121 renderer status line.
124 logcat: A string to parse. Can include line separators.
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.
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':
137 browser_success = (status == 'SUCCESS')
138 elif process_type == 'RENDERER':
139 renderer_found = True
140 renderer_success = (status == 'SUCCESS')
142 assert False, 'Invalid process type ' + process_type
144 if browser_found and renderer_found:
145 return (True, browser_success, renderer_success)
147 # Didn't find anything.
148 return (False, None, None)
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|.
155 device: A DeviceUtils instance.
156 timeout: Timeout in seconds
158 A (status, logs) tuple, where status is a ResultType constant, and logs
159 if the final logcat output as a string.
161 # 1. Start recording logcat with appropriate filters.
162 device.old_interface.StartRecordingLogcat(
163 clear=True, filters=_LOGCAT_FILTERS)
166 # 2. Force-start activity.
167 device.old_interface.StartActivity(
168 package=_PACKAGE_NAME, activity=_ACTIVITY_NAME, force_stop=True)
170 # 3. Wait up to |timeout| seconds until the test status is in the logcat.
174 while num_tries < max_tries:
177 found, browser_ok, renderer_ok = _CheckLinkerTestStatus(
178 device.old_interface.GetCurrentRecordedLogcat())
183 logs = device.old_interface.StopRecordingLogcat()
185 if num_tries >= max_tries:
186 return ResultType.TIMEOUT, logs
188 if browser_ok and renderer_ok:
189 return ResultType.PASS, logs
191 return ResultType.FAIL, logs
194 class LibraryLoadMap(dict):
195 """A helper class to pretty-print a map of library names to load addresses."""
197 items = ['\'%s\': 0x%x' % (name, address) for \
198 (name, address) in self.iteritems()]
199 return '{%s}' % (', '.join(items))
202 return 'LibraryLoadMap(%s)' % self.__str__()
205 class AddressList(list):
206 """A helper class to pretty-print a list of load addresses."""
208 items = ['0x%x' % address for address in self]
209 return '[%s]' % (', '.join(items))
212 return 'AddressList(%s)' % self.__str__()
215 def _ExtractLibraryLoadAddressesFromLogcat(logs):
216 """Extract the names and addresses of shared libraries loaded in the
217 browser and renderer processes.
219 logs: A string containing logcat output.
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.
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
235 assert False, 'Invalid process type'
237 return browser_libs, renderer_libs
240 def _CheckLoadAddressRandomization(lib_map_list, process_type):
241 """Check that a map of library load addresses is random enough.
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.
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.
252 # Collect, for each library, its list of load addresses.
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)
260 logging.info('%s library load map: %s', process_type, lib_addr_map)
262 # For each library, check the randomness of its load addresses.
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
274 return False, '%s libraries failed randomization: %s' % \
275 (process_type, bad_libs)
277 return True, '%s libraries properly randomized: %s' % \
278 (process_type, lib_addr_map)
281 class LinkerTestCaseBase(object):
282 """Base class for linker test cases."""
284 def __init__(self, is_low_memory=False):
285 """Create a test case.
287 is_low_memory: True to simulate a low-memory device, False otherwise.
289 self.is_low_memory = is_low_memory
291 test_suffix = 'ForLowMemoryDevice'
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
298 def _RunTest(self, _device):
299 """Run the test, must be overriden.
301 _device: A DeviceUtils interface.
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
307 return ResultType.FAIL, 'Unimplemented _RunTest() method!'
309 def Run(self, device):
310 """Run the test on a given device.
312 device: Name of target device where to run the test.
314 A base_test_result.TestRunResult() instance.
317 print '[ %-*s ] %s' % (margin, 'RUN', self.tagged_name)
318 logging.info('Running linker test: %s', self.tagged_name)
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)
327 status, logs = self._RunTest(device)
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)
336 results = base_test_result.TestRunResults()
338 base_test_result.BaseTestResult(
346 return self.tagged_name
349 return self.tagged_name
352 class LinkerSharedRelroTest(LinkerTestCaseBase):
353 """A linker test case to check the status of shared RELRO sections.
355 The core of the checks performed here are pretty simple:
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:
363 BROWSER_LINKER_TEST: <status>
364 RENDERER_LINKER_TEST: <status>
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.
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.
373 - The test case passes if the <status> for both the browser and renderer
374 process are SUCCESS. Otherwise its a fail.
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)
381 class LinkerLibraryAddressTest(LinkerTestCaseBase):
382 """A test case that verifies library load addresses.
384 The point of this check is to ensure that the libraries are loaded
385 according to the following rules:
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.
390 - For regular devices, the browser process should load libraries above
391 0x4000_0000, and renderer ones below it.
393 def _RunTest(self, device):
394 result, logs = _StartActivityAndWaitForLinkerTestStatus(device, timeout=30)
396 # Return immediately in case of timeout.
397 if result == ResultType.TIMEOUT:
400 # Collect the library load addresses in the browser and renderer processes.
401 browser_libs, renderer_libs = _ExtractLibraryLoadAddressesFromLogcat(logs)
403 logging.info('Browser libraries: %s', browser_libs)
404 logging.info('Renderer libraries: %s', renderer_libs)
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
414 # And that there are not empty.
416 logging.error('No libraries loaded in any process!')
417 return ResultType.FAIL, logs
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
423 for lib_name, lib_address in renderer_libs.iteritems():
424 if lib_address >= memory_boundary:
425 bad_libs.append((lib_name, lib_address))
428 logging.error('Renderer libraries loaded at high addresses: %s', bad_libs)
429 return ResultType.FAIL, logs
431 browser_config = _GetBrowserSharedRelroConfig()
432 if not browser_config:
433 return ResultType.FAIL, 'Bad linker source configuration'
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.
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))
446 logging.error('Library load address mismatches: %s',
448 return ResultType.FAIL, logs
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.
455 for lib_name, lib_address in browser_libs.iteritems():
456 if lib_address < memory_boundary:
457 bad_libs.append((lib_name, lib_address))
460 logging.error('Browser libraries loaded at low addresses: %s', bad_libs)
461 return ResultType.FAIL, logs
464 return ResultType.PASS, logs
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.
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.
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.
480 def _RunTest(self, device):
482 browser_lib_map_list = []
483 renderer_lib_map_list = []
485 for _ in range(max_loops):
486 # Start the activity.
487 result, logs = _StartActivityAndWaitForLinkerTestStatus(
489 if result == ResultType.TIMEOUT:
490 # Something bad happened. Return immediately.
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)
499 # Check randomization in the browser libraries.
500 logs = '\n'.join(logs_list)
502 browser_status, browser_logs = _CheckLoadAddressRandomization(
503 browser_lib_map_list, 'Browser')
505 renderer_status, renderer_logs = _CheckLoadAddressRandomization(
506 renderer_lib_map_list, 'Renderer')
508 browser_config = _GetBrowserSharedRelroConfig()
509 if not browser_config:
510 return ResultType.FAIL, 'Bad linker source configuration'
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
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".
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).
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.
529 # This has been tested experimentally on both Android 4.1.2 and 4.3.
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')
537 if not renderer_status:
538 return ResultType.FAIL, renderer_logs
540 return ResultType.PASS, logs