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

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

Powered by Google App Engine
This is Rietveld 408576698