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