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

Side by Side Diff: build/android/pylib/linker/test_case.py

Issue 2101243005: Add a snapshot of flutter/engine/src/build to our sdk (Closed) Base URL: git@github.com:dart-lang/sdk.git@master
Patch Set: add README.dart Created 4 years, 5 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
« 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 »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(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
OLDNEW
« 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