Chromium Code Reviews| Index: build/android/pylib/linker/test_case.py |
| diff --git a/build/android/pylib/linker/test_case.py b/build/android/pylib/linker/test_case.py |
| index e387c0388a558da6cb96c692cb2c120953367cec..f7a6c241bd0535ffff13554701a026a96a3582ef 100644 |
| --- a/build/android/pylib/linker/test_case.py |
| +++ b/build/android/pylib/linker/test_case.py |
| @@ -32,26 +32,6 @@ |
| ninja -C out/Debug content_linker_test_apk |
| build/android/test_runner.py linker |
| - The core of the checks performed here are pretty simple: |
| - |
| - - Clear the logcat and start recording with an appropriate set of filters. |
| - - Create the command-line appropriate for the test-case. |
| - - Start the activity (always forcing a cold start). |
| - - Every second, look at the current content of the filtered logcat lines |
| - and look for instances of the following: |
| - |
| - BROWSER_LINKER_TEST: <status> |
| - RENDERER_LINKER_TEST: <status> |
| - |
| - where <status> can be either FAIL or SUCCESS. These lines can appear |
| - in any order in the logcat. Once both browser and renderer status are |
| - found, stop the loop. Otherwise timeout after 30 seconds. |
| - |
| - Note that there can be other lines beginning with BROWSER_LINKER_TEST: |
| - and RENDERER_LINKER_TEST:, but are not followed by a <status> code. |
| - |
| - - The test case passes if the <status> for both the browser and renderer |
| - process are SUCCESS. Otherwise its a fail. |
| """ |
| import logging |
| @@ -66,6 +46,7 @@ from pylib import android_commands |
| from pylib import flag_changer |
| from pylib.base import base_test_result |
| +ResultType = base_test_result.ResultType |
| _PACKAGE_NAME='org.chromium.content_linker_test_apk' |
| _ACTIVITY_NAME='.ContentLinkerTestActivity' |
| @@ -76,10 +57,23 @@ _COMMAND_LINE_FILE='/data/local/tmp/content-linker-test-command-line' |
| # it is handy to have the 'content_android_linker' ones as well when |
| # troubleshooting. |
| _LOGCAT_FILTERS = [ '*:s', 'chromium:v', 'content_android_linker:v' ] |
| +#_LOGCAT_FILTERS = [ '*:v' ] ## DEBUG |
| # Regular expression used to match status lines in logcat. |
| re_status_line = re.compile(r'(BROWSER|RENDERER)_LINKER_TEST: (FAIL|SUCCESS)') |
| +# Regular expression used to mach library load addresses in logcat. |
| +re_library_address = re.compile( |
| + r'(BROWSER|RENDERER)_LIBRARY_ADDRESS: (\S+) ([0-9A-Fa-f]+)') |
| + |
| + |
| +def _WriteCommandLineFile(adb, command_line, command_line_file): |
| + """Create a command-line file on the device. This does not use FlagChanger |
| + because its implementation assumes the device has 'su', and thus does |
| + not work at all with production devices.""" |
|
bulach
2013/10/07 16:49:44
would this work on directory that's not writable b
digit1
2013/10/07 20:31:58
No, the main issue is that FlaChanger really runs
|
| + adb.RunShellCommand('echo "%s" > %s' % (command_line, command_line_file)) |
| + |
| + |
| def _CheckLinkerTestStatus(logcat): |
| """Parse the content of |logcat| and checks for both a browser and |
| renderer status line. |
| @@ -112,94 +106,212 @@ def _CheckLinkerTestStatus(logcat): |
| return (False, None, None) |
| -def _CreateCommandLineFileOnDevice(adb, flags): |
| - changer = flag_changer.FlagChanger(adb, _COMMAND_LINE_FILE) |
| - changer.Set(flags) |
| +def _WaitForLinkerTestStatus(adb, timeout): |
| + """Wait up to |timeout| seconds until the full linker test status lines appear |
| + in the logcat being recorded with |adb|. |
| + Args: |
| + adb: An AndroidCommands instance. This assumes adb.StartRecordingLogcat() |
| + was called previously. |
| + timeout: Timeout in seconds. |
| + Returns: |
| + ResultType.TIMEOUT in case of timeout, ResulType.PASS if both status lines |
| + report 'SUCCESS', or ResulType.FAIL otherwise. |
| + """ |
| -class LinkerTestCase(object): |
| - """Base class for linker test cases.""" |
| +def _StartActivityAndWaitForLinkerTestStatus(adb, timeout): |
| + """Force-start an activity and wait up to |timeout| seconds until the full |
| + linker test status lines appear in the logcat, recorded through |adb|. |
| + Args: |
| + adb: An AndroidCommands instance. |
| + timeout: Timeout in seconds |
| + Returns: |
| + A (status, logs) tuple, where status is a ResultType constant, and logs |
| + if the final logcat output as a string. |
| + """ |
| + # 1. Start recording logcat with appropriate filters. |
| + adb.StartRecordingLogcat(clear=True, filters=_LOGCAT_FILTERS) |
| + |
| + try: |
| + # 2. Force-start activity. |
| + adb.StartActivity(package=_PACKAGE_NAME, |
| + activity=_ACTIVITY_NAME, |
| + force_stop=True) |
| + |
| + # 3. Wait up to |timeout| seconds until the test status is in the logcat. |
| + num_tries = 0 |
| + max_tries = timeout |
| + found = False |
| + while num_tries < max_tries: |
| + time.sleep(1) |
| + num_tries += 1 |
| + found, browser_ok, renderer_ok = _CheckLinkerTestStatus( |
| + adb.GetCurrentRecordedLogcat()) |
| + if found: |
| + break |
| + |
| + finally: |
| + logs = adb.StopRecordingLogcat() |
| + |
| + if num_tries >= max_tries: |
| + return ResultType.TIMEOUT, logs |
| + |
| + if browser_ok and renderer_ok: |
| + return ResultType.PASS, logs |
| - def __init__(self, test_name, is_low_memory=False): |
| - """Create a test case initialized to run |test_name|. |
| + return ResultType.FAIL, logs |
| + |
| +class LibraryLoadMap(dict): |
| + """A helper class to pretty-print a map of library names to load addresses.""" |
| + def __str__(self): |
| + items = ['\'%s\': 0x%x' % (name, address) for \ |
| + (name, address) in self.iteritems()] |
| + return '{%s}' % (', '.join(items)) |
| + |
| + def __repr__(self): |
| + return 'LibraryLoadMap(%s)' % self.__str__() |
| + |
| + |
| +class AddressList(list): |
| + """A helper class to pretty-print a list of load addresses.""" |
| + def __str__(self): |
| + items = ['0x%x' % address for address in self] |
| + return '[%s]' % (', '.join(items)) |
| + |
| + def __repr__(self): |
| + return 'AddressList(%s)' % self.__str__() |
| + |
| + |
| +def _ExtractLibraryLoadAddressesFromLogcat(logs): |
| + """Extract the names and addresses of shared libraries loaded in the |
| + browser and renderer processes. |
| + Args: |
| + logs: A string containing logcat output. |
| + Returns: |
| + A tuple (browser_libs, renderer_libs), where each item is a map of |
| + library names (strings) to library load addresses (ints), for the |
| + browser and renderer processes, respectively. |
| + """ |
| + browser_libs = LibraryLoadMap() |
| + renderer_libs = LibraryLoadMap() |
| + for m in re_library_address.finditer(logs): |
| + process_type, lib_name, lib_address = m.groups() |
| + lib_address = int(lib_address, 16) |
| + if process_type == 'BROWSER': |
| + browser_libs[lib_name] = lib_address |
| + elif process_type == 'RENDERER': |
| + renderer_libs[lib_name] = lib_address |
| + else: |
| + assert False, 'Invalid process type' |
| + |
| + return browser_libs, renderer_libs |
| + |
| + |
| +def _CheckLoadAddressRandomization(lib_map_list, process_type): |
| + """Check that a map of library load addresses is random enough. |
| + Args: |
| + lib_map_list: a list of dictionaries that map library names (string) |
| + to load addresses (int). Each item in the list corresponds to a |
| + different run / process start. |
| + process_type: a string describing the process type. |
| + Returns: |
| + (status, logs) tuple, where <status> is True iff the load addresses are |
| + randomized, False otherwise, and <logs> is a string containing an error |
| + message detailing the libraries that are not randomized properly. |
| + """ |
| + # Collect, for each library, its list of load addresses. |
| + lib_addr_map = {} |
| + for lib_map in lib_map_list: |
| + for lib_name, lib_address in lib_map.iteritems(): |
| + if lib_name not in lib_addr_map: |
| + lib_addr_map[lib_name] = AddressList() |
| + lib_addr_map[lib_name].append(lib_address) |
| + |
| + logging.info('%s library load map: %s' % (process_type, lib_addr_map)) |
|
bulach
2013/10/07 16:49:44
nit: logging can take a format with vararg, so rat
|
| + |
| + # For each library, check the randomness of its load addresses. |
| + bad_libs = {} |
| + success = True |
| + for lib_name, lib_address_list in lib_addr_map.iteritems(): |
| + # If all addresses are different, skip to next item. |
| + lib_address_set = set(lib_address_list) |
| + # Consider that if there is more than one pair of identical addresses in |
| + # the list, then randomization is broken. |
| + if len(lib_address_set) < len(lib_address_list) - 1: |
| + bad_libs[lib_name] = lib_address_list |
| + |
| + |
| + if bad_libs: |
| + return False, '%s libraries failed randomization: %s' % \ |
| + (process_type, bad_libs) |
| + |
| + return True, '%s libraries properly randomized: %s' % \ |
| + (process_type, lib_addr_map) |
| + |
| + |
| +class LinkerTestCaseBase(object): |
| + """Base class for linker test cases.""" |
| + |
| + def __init__(self, is_low_memory=False): |
| + """Create a test case. |
| Args: |
| - test_name: The name of the method to run as the test. |
| is_low_memory: True to simulate a low-memory device, False otherwise. |
| """ |
| - self.test_name = test_name |
| + self.is_low_memory = is_low_memory |
| + if is_low_memory: |
| + test_suffix = 'ForLowMemoryDevice' |
| + else: |
| + test_suffix = 'ForRegularDevice' |
| class_name = self.__class__.__name__ |
| - self.qualified_name = '%s.%s' % (class_name, self.test_name) |
| + self.qualified_name = '%s.%s' % (class_name, test_suffix) |
| self.tagged_name = self.qualified_name |
| - self.is_low_memory = is_low_memory |
| + |
| + def _RunTest(self, adb): |
| + """Run the test, must be overriden. |
| + Args: |
| + adb: An AndroidCommands instance to the device. |
| + Returns: |
| + A (status, log) tuple, where <status> is a ResultType constant, and <log> |
| + is the logcat output captured during the test in case of error, or None |
| + in case of success. |
| + """ |
| + return ResultType.FAIL, 'Unimplemented _RunTest() method!' |
| def Run(self, device): |
| + """Run the test on a given device. |
| + Args: |
| + device: Name of target device where to run the test. |
| + Returns: |
| + A base_test_result.TestRunResult() instance. |
| + """ |
| margin = 8 |
| print '[ %-*s ] %s' % (margin, 'RUN', self.tagged_name) |
| logging.info('Running linker test: %s', self.tagged_name) |
| adb = android_commands.AndroidCommands(device) |
| - # 1. Write command-line file with appropriate options. |
| - command_line_flags = [] |
| + # Create command-line file on device. |
| + command_line_flags = '' |
| if self.is_low_memory: |
| - command_line_flags.append('--low-memory-device') |
| - _CreateCommandLineFileOnDevice(adb, command_line_flags) |
| - |
| - # 2. Start recording logcat with appropriate filters. |
| - adb.StartRecordingLogcat(clear=True, filters=_LOGCAT_FILTERS) |
| - |
| - try: |
| - # 3. Force-start activity. |
| - adb.StartActivity(package=_PACKAGE_NAME, |
| - activity=_ACTIVITY_NAME, |
| - force_stop=True) |
| - |
| - # 4. Wait up to 30 seconds until the linker test status is in the logcat. |
| - max_tries = 30 |
| - num_tries = 0 |
| - found = False |
| - logcat = None |
| - while num_tries < max_tries: |
| - time.sleep(1) |
| - num_tries += 1 |
| - found, browser_ok, renderer_ok = _CheckLinkerTestStatus( |
| - adb.GetCurrentRecordedLogcat()) |
| - if found: |
| - break |
| - |
| - finally: |
| - # Ensure the ADB polling process is always killed when |
| - # the script is interrupted by the user with Ctrl-C. |
| - logs = adb.StopRecordingLogcat() |
| + command_line_flags = '--low-memory-device' |
| + _WriteCommandLineFile(adb, command_line_flags, _COMMAND_LINE_FILE) |
| - results = base_test_result.TestRunResults() |
| + # Run the test. |
| + status, logs = self._RunTest(adb) |
| - if num_tries >= max_tries: |
| - # Timeout |
| - print '[ %*s ] %s' % (margin, 'TIMEOUT', self.tagged_name) |
| - results.AddResult( |
| - base_test_result.BaseTestResult( |
| - self.test_name, |
| - base_test_result.ResultType.TIMEOUT, |
| - logs)) |
| - elif browser_ok and renderer_ok: |
| - # Passed |
| - logging.info( |
| - 'Logcat start ---------------------------------\n%s' + |
| - 'Logcat end -----------------------------------', logs) |
| - print '[ %*s ] %s' % (margin, 'OK', self.tagged_name) |
| - results.AddResult( |
| - base_test_result.BaseTestResult( |
| - self.test_name, |
| - base_test_result.ResultType.PASS)) |
| - else: |
| - print '[ %*s ] %s' % (margin, 'FAILED', self.tagged_name) |
| - # Failed |
| - results.AddResult( |
| - base_test_result.BaseTestResult( |
| - self.test_name, |
| - base_test_result.ResultType.FAIL, |
| - logs)) |
| + result_text = 'OK' |
| + if status == ResultType.FAIL: |
| + result_text = 'FAILED' |
| + elif status == ResultType.TIMEOUT: |
| + result_text = 'TIMEOUT' |
| + print '[ %*s ] %s' % (margin, result_text, self.tagged_name) |
| + |
| + results = base_test_result.TestRunResults() |
| + results.AddResult( |
| + base_test_result.BaseTestResult( |
| + self.tagged_name, |
| + status, |
| + logs)) |
| return results |
| @@ -208,3 +320,186 @@ class LinkerTestCase(object): |
| def __repr__(self): |
| return self.tagged_name |
| + |
| + |
| +class LinkerSharedRelroTest(LinkerTestCaseBase): |
| + """A linker test case to check the status of shared RELRO sections. |
| + |
| + The core of the checks performed here are pretty simple: |
| + |
| + - Clear the logcat and start recording with an appropriate set of filters. |
| + - Create the command-line appropriate for the test-case. |
| + - Start the activity (always forcing a cold start). |
| + - Every second, look at the current content of the filtered logcat lines |
| + and look for instances of the following: |
| + |
| + BROWSER_LINKER_TEST: <status> |
| + RENDERER_LINKER_TEST: <status> |
| + |
| + where <status> can be either FAIL or SUCCESS. These lines can appear |
| + in any order in the logcat. Once both browser and renderer status are |
| + found, stop the loop. Otherwise timeout after 30 seconds. |
| + |
| + Note that there can be other lines beginning with BROWSER_LINKER_TEST: |
| + and RENDERER_LINKER_TEST:, but are not followed by a <status> code. |
| + |
| + - The test case passes if the <status> for both the browser and renderer |
| + process are SUCCESS. Otherwise its a fail. |
| + """ |
| + def _RunTest(self, adb): |
| + # Wait up to 30 seconds until the linker test status is in the logcat. |
| + return _StartActivityAndWaitForLinkerTestStatus(adb, timeout=30) |
| + |
| + |
| +class LinkerLibraryAddressTest(LinkerTestCaseBase): |
| + """A test case that verifies library load addresses. |
| + |
| + The point of this check is to ensure that the libraries are loaded |
| + according to the following rules: |
| + |
| + - For low-memory devices, they should always be loaded at the same address |
| + in both browser and renderer processes, both below 0x4000_0000. |
| + |
| + - For regular devices, the browser process should load libraries above |
| + 0x4000_0000, and renderer ones below it. |
| + """ |
| + def _RunTest(self, adb): |
| + result, logs = _StartActivityAndWaitForLinkerTestStatus(adb, timeout=30) |
| + |
| + # Return immediately in case of timeout. |
| + if result == ResultType.TIMEOUT: |
| + return result, logs |
| + |
| + # Collect the library load addresses in the browser and renderer processes. |
| + browser_libs, renderer_libs = _ExtractLibraryLoadAddressesFromLogcat(logs) |
| + |
| + logging.info('Browser libraries: %s' % browser_libs) |
| + logging.info('Renderer libraries: %s' % renderer_libs) |
|
bulach
2013/10/07 16:49:44
nit: as above, prefer , rather than % and some pla
|
| + |
| + # Check that the same libraries are loaded into both processes: |
| + browser_set = set(browser_libs.keys()) |
| + renderer_set = set(renderer_libs.keys()) |
| + if browser_set != renderer_set: |
| + logging.error('Library set mistmach browser=%s renderer=%s' % ( |
| + browser_libs.keys(), renderer_libs.keys())) |
| + return ResultType.FAIL, logs |
| + |
| + # And that there are not empty. |
| + if not browser_set: |
| + logging.error('No libraries loaded in any process!') |
| + return ResultType.FAIL, logs |
| + |
| + # Check that the renderer libraries are loaded at 'low-addresses'. i.e. |
| + # below 0x4000_0000, for every kind of device. |
| + memory_boundary = 0x40000000 |
| + bad_libs = [] |
| + for lib_name, lib_address in renderer_libs.iteritems(): |
| + if lib_address >= memory_boundary: |
| + bad_libs.append((lib_name, lib_address)) |
| + |
| + if bad_libs: |
| + logging.error('Renderer libraries loaded at high addresses: %s' % \ |
| + bad_libs) |
| + return ResultType.FAIL, logs |
| + |
| + if self.is_low_memory: |
| + # For low-memory devices, the libraries must all be loaded at the same |
| + # addresses. This also implicitly checks that the browser libraries are at |
| + # low addresses. |
| + addr_mismatches = [] |
| + for lib_name, lib_address in browser_libs.iteritems(): |
| + lib_address2 = renderer_libs[lib_name] |
| + if lib_address != lib_address2: |
| + addr_mismatches.append((lib_name, lib_address, lib_address2)) |
| + |
| + if addr_mismatches: |
| + logging.error('Library load address mismatches: %s' % \ |
| + addr_mismatches) |
| + return ResultType.FAIL, logs |
| + |
| + # For regular devices, check that libraries are loaded at 'high-addresses'. |
| + # Note that for low-memory devices, the previous checks ensure that they |
| + # were loaded at low-addresses. |
| + if not self.is_low_memory: |
| + bad_libs = [] |
| + for lib_name, lib_address in browser_libs.iteritems(): |
| + if lib_address < memory_boundary: |
| + bad_libs.append((lib_name, lib_address)) |
| + |
| + if bad_libs: |
| + logging.error('Browser libraries loaded at low addresses: %s' % \ |
| + repr(bad_libs)) |
| + return ResultType.FAIL, logs |
| + |
| + # Everything's ok. |
| + return ResultType.PASS, logs |
| + |
| + |
| +class LinkerRandomizationTest(LinkerTestCaseBase): |
| + """A linker test case to check that library load address randomization works |
| + properly between successive starts of the test program/activity. |
| + |
| + This starts the activity several time (each time forcing a new process |
| + creation) and compares the load addresses of the libraries in them to |
| + detect that they have changed. |
| + |
| + In theory, two successive runs could (very rarely) use the same load |
| + address, so loop 5 times and compare the values there. It is assumed |
| + that if there are more than one pair of identical addresses, then the |
| + load addresses are not random enough for this test. |
| + """ |
| + def _RunTest(self, adb): |
| + max_loops = 5 |
| + browser_lib_map_list = [] |
| + renderer_lib_map_list = [] |
| + logs_list = [] |
| + for loop in range(max_loops): |
| + # Start the activity. |
| + result, logs = _StartActivityAndWaitForLinkerTestStatus(adb, timeout=30) |
| + if result == ResultType.TIMEOUT: |
| + # Something bad happened. Return immediately. |
| + return result, logs |
| + |
| + # Collect library addresses. |
| + browser_libs, renderer_libs = _ExtractLibraryLoadAddressesFromLogcat(logs) |
| + browser_lib_map_list.append(browser_libs) |
| + renderer_lib_map_list.append(renderer_libs) |
| + logs_list.append(logs) |
| + |
| + # Check randomization in the browser libraries. |
| + logs = '\n'.join(logs_list) |
| + |
| + browser_status, browser_logs = _CheckLoadAddressRandomization( |
| + browser_lib_map_list, 'Browser') |
| + |
| + renderer_status, renderer_logs = _CheckLoadAddressRandomization( |
| + renderer_lib_map_list, 'Renderer') |
| + |
| + if not browser_status: |
| + if self.is_low_memory: |
| + return ResultType.FAIL, browser_logs |
| + |
| + # IMPORTANT NOTE: The system's ASLR implementation seems to be very poor |
| + # when starting an activity process in a loop with "adb shell am start". |
| + # |
| + # When simulating a regular device, loading libraries in the browser |
| + # process uses a simple mmap(NULL, ...) to let the kernel device where to |
| + # load the file (this is similar to what System.loadLibrary() does). |
| + # |
| + # Unfortunately, at least in the context of this test, doing so while |
| + # restarting the activity with the activity manager very, very, often |
| + # results in the system using the same load address for all 5 runs, or |
| + # sometimes only 4 out of 5. |
| + # |
| + # This has been tested experimentally on both Android 4.1.2 and 4.3. |
| + # |
| + # Note that this behaviour doesn't seem to happen when starting an |
| + # application 'normally', i.e. when using the application launcher to |
| + # start the activity. |
| + logging.info('Ignoring system\'s low randomization of browser libraries' + |
| + ' for regular devices') |
| + |
| + if not renderer_status: |
| + return ResultType.FAIL, renderer_logs |
| + |
| + return ResultType.PASS, logs |