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