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 content_linker_test_apk 33 build/android/test_runner.py linker 34 35 """ 36 37 import logging 38 import os 39 import re 40 import StringIO 41 import subprocess 42 import tempfile 43 import time 44 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 49 50 ResultType = base_test_result.ResultType 51 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' 55 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' 59 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) 66 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 70 # troubleshooting. 71 _LOGCAT_FILTERS = [ '*:s', 'chromium:v', 'content_android_linker:v' ] 72 #_LOGCAT_FILTERS = [ '*:v' ] ## DEBUG 73 74 # Regular expression used to match status lines in logcat. 75 re_status_line = re.compile(r'(BROWSER|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 _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)) 119 120 121 def _CheckLinkerTestStatus(logcat): 122 """Parse the content of |logcat| and checks for both a browser and 123 renderer status line. 124 125 Args: 126 logcat: A string to parse. Can include line separators. 127 128 Returns: 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. 132 """ 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': 138 browser_found = True 139 browser_success = (status == 'SUCCESS') 140 elif process_type == 'RENDERER': 141 renderer_found = True 142 renderer_success = (status == 'SUCCESS') 143 else: 144 assert False, 'Invalid process type ' + process_type 145 146 if browser_found and renderer_found: 147 return (True, browser_success, renderer_success) 148 149 # Didn't find anything. 150 return (False, None, None) 151 152 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|. 156 Args: 157 adb: An AndroidCommands instance. This assumes adb.StartRecordingLogcat() 158 was called previously. 159 timeout: Timeout in seconds. 160 Returns: 161 ResultType.TIMEOUT in case of timeout, ResulType.PASS if both status lines 162 report 'SUCCESS', or ResulType.FAIL otherwise. 163 """ 164 165 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|. 169 Args: 170 adb: An AndroidCommands instance. 171 timeout: Timeout in seconds 172 Returns: 173 A (status, logs) tuple, where status is a ResultType constant, and logs 174 if the final logcat output as a string. 175 """ 176 # 1. Start recording logcat with appropriate filters. 177 adb.StartRecordingLogcat(clear=True, filters=_LOGCAT_FILTERS) 178 179 try: 180 # 2. Force-start activity. 181 adb.StartActivity(package=_PACKAGE_NAME, 182 activity=_ACTIVITY_NAME, 183 force_stop=True) 184 185 # 3. Wait up to |timeout| seconds until the test status is in the logcat. 186 num_tries = 0 187 max_tries = timeout 188 found = False 189 while num_tries < max_tries: 190 time.sleep(1) 191 num_tries += 1 192 found, browser_ok, renderer_ok = _CheckLinkerTestStatus( 193 adb.GetCurrentRecordedLogcat()) 194 if found: 195 break 196 197 finally: 198 logs = adb.StopRecordingLogcat() 199 200 if num_tries >= max_tries: 201 return ResultType.TIMEOUT, logs 202 203 if browser_ok and renderer_ok: 204 return ResultType.PASS, logs 205 206 return ResultType.FAIL, logs 207 208 209 class LibraryLoadMap(dict): 210 """A helper class to pretty-print a map of library names to load addresses.""" 211 def __str__(self): 212 items = ['\'%s\': 0x%x' % (name, address) for \ 213 (name, address) in self.iteritems()] 214 return '{%s}' % (', '.join(items)) 215 216 def __repr__(self): 217 return 'LibraryLoadMap(%s)' % self.__str__() 218 219 220 class AddressList(list): 221 """A helper class to pretty-print a list of load addresses.""" 222 def __str__(self): 223 items = ['0x%x' % address for address in self] 224 return '[%s]' % (', '.join(items)) 225 226 def __repr__(self): 227 return 'AddressList(%s)' % self.__str__() 228 229 230 def _ExtractLibraryLoadAddressesFromLogcat(logs): 231 """Extract the names and addresses of shared libraries loaded in the 232 browser and renderer processes. 233 Args: 234 logs: A string containing logcat output. 235 Returns: 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. 239 """ 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 249 else: 250 assert False, 'Invalid process type' 251 252 return browser_libs, renderer_libs 253 254 255 def _CheckLoadAddressRandomization(lib_map_list, process_type): 256 """Check that a map of library load addresses is random enough. 257 Args: 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. 262 Returns: 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. 266 """ 267 # Collect, for each library, its list of load addresses. 268 lib_addr_map = {} 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) 274 275 logging.info('%s library load map: %s', process_type, lib_addr_map) 276 277 # For each library, check the randomness of its load addresses. 278 bad_libs = {} 279 success = True 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 287 288 289 if bad_libs: 290 return False, '%s libraries failed randomization: %s' % \ 291 (process_type, bad_libs) 292 293 return True, '%s libraries properly randomized: %s' % \ 294 (process_type, lib_addr_map) 295 296 297 class LinkerTestCaseBase(object): 298 """Base class for linker test cases.""" 299 300 def __init__(self, is_low_memory=False): 301 """Create a test case. 302 Args: 303 is_low_memory: True to simulate a low-memory device, False otherwise. 304 """ 305 self.is_low_memory = is_low_memory 306 if is_low_memory: 307 test_suffix = 'ForLowMemoryDevice' 308 else: 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 313 314 def _RunTest(self, adb): 315 """Run the test, must be overriden. 316 Args: 317 adb: An AndroidCommands instance to the device. 318 Returns: 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 321 in case of success. 322 """ 323 return ResultType.FAIL, 'Unimplemented _RunTest() method!' 324 325 def Run(self, device): 326 """Run the test on a given device. 327 Args: 328 device: Name of target device where to run the test. 329 Returns: 330 A base_test_result.TestRunResult() instance. 331 """ 332 margin = 8 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) 336 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) 342 343 # Run the test. 344 status, logs = self._RunTest(adb) 345 346 result_text = 'OK' 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) 352 353 results = base_test_result.TestRunResults() 354 results.AddResult( 355 base_test_result.BaseTestResult( 356 self.tagged_name, 357 status, 358 logs)) 359 360 return results 361 362 def __str__(self): 363 return self.tagged_name 364 365 def __repr__(self): 366 return self.tagged_name 367 368 369 class LinkerSharedRelroTest(LinkerTestCaseBase): 370 """A linker test case to check the status of shared RELRO sections. 371 372 The core of the checks performed here are pretty simple: 373 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: 379 380 BROWSER_LINKER_TEST: <status> 381 RENDERER_LINKER_TEST: <status> 382 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. 386 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. 389 390 - The test case passes if the <status> for both the browser and renderer 391 process are SUCCESS. Otherwise its a fail. 392 """ 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) 396 397 398 class LinkerLibraryAddressTest(LinkerTestCaseBase): 399 """A test case that verifies library load addresses. 400 401 The point of this check is to ensure that the libraries are loaded 402 according to the following rules: 403 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. 406 407 - For regular devices, the browser process should load libraries above 408 0x4000_0000, and renderer ones below it. 409 """ 410 def _RunTest(self, adb): 411 result, logs = _StartActivityAndWaitForLinkerTestStatus(adb, timeout=30) 412 413 # Return immediately in case of timeout. 414 if result == ResultType.TIMEOUT: 415 return result, logs 416 417 # Collect the library load addresses in the browser and renderer processes. 418 browser_libs, renderer_libs = _ExtractLibraryLoadAddressesFromLogcat(logs) 419 420 logging.info('Browser libraries: %s', browser_libs) 421 logging.info('Renderer libraries: %s', renderer_libs) 422 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 430 431 # And that there are not empty. 432 if not browser_set: 433 logging.error('No libraries loaded in any process!') 434 return ResultType.FAIL, logs 435 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 439 bad_libs = [] 440 for lib_name, lib_address in renderer_libs.iteritems(): 441 if lib_address >= memory_boundary: 442 bad_libs.append((lib_name, lib_address)) 443 444 if bad_libs: 445 logging.error('Renderer libraries loaded at high addresses: %s', bad_libs) 446 return ResultType.FAIL, logs 447 448 browser_config = _GetBrowserSharedRelroConfig() 449 if not browser_config: 450 return ResultType.FAIL, 'Bad linker source configuration' 451 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. 456 addr_mismatches = [] 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)) 461 462 if addr_mismatches: 463 logging.error('Library load address mismatches: %s', 464 addr_mismatches) 465 return ResultType.FAIL, logs 466 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. 470 else: 471 bad_libs = [] 472 for lib_name, lib_address in browser_libs.iteritems(): 473 if lib_address < memory_boundary: 474 bad_libs.append((lib_name, lib_address)) 475 476 if bad_libs: 477 logging.error('Browser libraries loaded at low addresses: %s', bad_libs) 478 return ResultType.FAIL, logs 479 480 # Everything's ok. 481 return ResultType.PASS, logs 482 483 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. 487 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. 491 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. 496 """ 497 def _RunTest(self, adb): 498 max_loops = 5 499 browser_lib_map_list = [] 500 renderer_lib_map_list = [] 501 logs_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. 507 return result, logs 508 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) 514 515 # Check randomization in the browser libraries. 516 logs = '\n'.join(logs_list) 517 518 browser_status, browser_logs = _CheckLoadAddressRandomization( 519 browser_lib_map_list, 'Browser') 520 521 renderer_status, renderer_logs = _CheckLoadAddressRandomization( 522 renderer_lib_map_list, 'Renderer') 523 524 browser_config = _GetBrowserSharedRelroConfig() 525 if not browser_config: 526 return ResultType.FAIL, 'Bad linker source configuration' 527 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 532 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". 535 # 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). 539 # 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. 544 # 545 # This has been tested experimentally on both Android 4.1.2 and 4.3. 546 # 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') 552 553 if not renderer_status: 554 return ResultType.FAIL, renderer_logs 555 556 return ResultType.PASS, logs 557