Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(77)

Unified Diff: build/android/pylib/linker/test_case.py

Issue 26251003: android: Add 4 new linker tests. (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/src
Patch Set: Address Marcus' nits. Created 7 years, 2 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
« no previous file with comments | « build/android/pylib/linker/setup.py ('k') | build/android/pylib/linker/test_runner.py » ('j') | no next file with comments »
Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
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..b8a256676341f90169677342d5c8983e293566ce 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."""
+ 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)
+
+ # 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,184 @@ 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)
+
+ # 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', 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
« no previous file with comments | « build/android/pylib/linker/setup.py ('k') | build/android/pylib/linker/test_runner.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698