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