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