android: Add 4 new linker tests.

This extracts the build/android/ changes for Patch Set 32 of
https://codereview.chromium.org/23717023/

This adds 4 new linker tests to check the library load addresses
and their proper randomization when simulating low-memory and
regular devices.

Since there are now 6 linker tests, also add support for
 -f / --gtest_filter options to filter the tests to run,
e.g. to run all low-memory device tests, one can use:

  build/android/test_runner.py linker --gtest_filter='*LowMemory*'

BUG=287739
R=bulach@chromium.org, frankf@chromium.org, yfriedman@chromium.org

Review URL: https://codereview.chromium.org/26251003

git-svn-id: http://src.chromium.org/svn/trunk/src/build@227418 4ff67af0-8c30-449e-8e8b-ad334ec8d88c
This commit is contained in:
digit@chromium.org 2013-10-08 01:19:05 +00:00
Родитель 4fa9932145
Коммит d8db73faa7
4 изменённых файлов: 404 добавлений и 96 удалений

Просмотреть файл

@ -4,7 +4,6 @@
"""Setup for linker tests."""
import logging
import os
import sys
import types
@ -12,6 +11,12 @@ import types
import test_case
import test_runner
from pylib import constants
sys.path.insert(0,
os.path.join(constants.DIR_SOURCE_ROOT, 'build', 'util', 'lib',
'common'))
import unittest_util
def Setup(options, devices):
"""Creates a list of test cases and a runner factory.
@ -19,12 +24,20 @@ def Setup(options, devices):
Returns:
A tuple of (TestRunnerFactory, tests).
"""
test_cases = [
test_case.LinkerLibraryAddressTest,
test_case.LinkerSharedRelroTest,
test_case.LinkerRandomizationTest ]
all_tests = [
test_case.LinkerTestCase('ForRegularDevice',
is_low_memory=False),
test_case.LinkerTestCase('ForLowMemoryDevice',
is_low_memory=True) ]
low_memory_modes = [False, True]
all_tests = [t(is_low_memory=m) for t in test_cases for m in low_memory_modes]
if options.test_filter:
all_test_names = [ test.qualified_name for test in all_tests ]
filtered_test_names = unittest_util.FilterTestNames(all_test_names,
options.test_filter)
all_tests = [t for t in all_tests \
if t.qualified_name in filtered_test_names]
def TestRunnerFactory(device, shard_index):
return test_runner.LinkerTestRunner(

Просмотреть файл

@ -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):
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
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, test_name, is_low_memory=False):
"""Create a test case initialized to run |test_name|.
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
class_name = self.__class__.__name__
self.qualified_name = '%s.%s' % (class_name, self.test_name)
self.tagged_name = self.qualified_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, test_suffix)
self.tagged_name = self.qualified_name
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)
command_line_flags = '--low-memory-device'
_WriteCommandLineFile(adb, command_line_flags, _COMMAND_LINE_FILE)
# 2. Start recording logcat with appropriate filters.
adb.StartRecordingLogcat(clear=True, filters=_LOGCAT_FILTERS)
# Run the test.
status, logs = self._RunTest(adb)
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()
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()
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))
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

Просмотреть файл

@ -80,14 +80,14 @@ class LinkerTestRunner(base_test_runner.BaseTestRunner):
"""Sets up and runs a test case.
Args:
test: An object which is ostensibly a subclass of LinkerTestCase.
test: An object which is ostensibly a subclass of LinkerTestCaseBase.
Returns:
A TestRunResults object which contains the result produced by the test
and, in the case of a failure, the test that should be retried.
"""
assert isinstance(test, test_case.LinkerTestCase)
assert isinstance(test, test_case.LinkerTestCaseBase)
try:
results = test.Run(self.device)

Просмотреть файл

@ -121,6 +121,8 @@ def AddLinkerTestOptions(option_parser):
option_parser.commands_dict = {}
option_parser.example = '%prog linker'
option_parser.add_option('-f', '--gtest-filter', dest='test_filter',
help='googletest-style filter string.')
AddCommonOptions(option_parser)