1# Copyright 2016 The Chromium OS 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 5import grp 6import logging 7import os 8import pwd 9import re 10import shutil 11import signal 12import stat 13import subprocess 14 15import crash_test 16from autotest_lib.client.bin import utils 17from autotest_lib.client.common_lib import error 18 19 20CRASHER = 'crasher_nobreakpad' 21 22 23class UserCrashTest(crash_test.CrashTest): 24 """ 25 Base class for tests that verify crash reporting for user processes. Shared 26 functionality includes installing a crasher executable, generating Breakpad 27 symbols, running the crasher process, and verifying collection and sending. 28 """ 29 30 31 def setup(self): 32 """Copy the crasher source code under |srcdir| and build it.""" 33 src = os.path.join(os.path.dirname(__file__), 'crasher') 34 dest = os.path.join(self.srcdir, 'crasher') 35 shutil.copytree(src, dest) 36 37 os.chdir(dest) 38 utils.make() 39 40 41 def initialize(self, expected_tag='user', expected_version=None, 42 force_user_crash_dir=False): 43 """Initialize and configure the test. 44 45 @param expected_tag: Expected tag in crash_reporter log message. 46 @param expected_version: Expected version included in the crash report, 47 or None to use the Chrome OS version. 48 @param force_user_crash_dir: Always look for crash reports in the crash 49 directory of the current user session, or 50 the fallback directory if no sessions. 51 """ 52 crash_test.CrashTest.initialize(self) 53 self._expected_tag = expected_tag 54 self._expected_version = expected_version 55 self._force_user_crash_dir = force_user_crash_dir 56 57 58 def _prepare_crasher(self, root_path='/'): 59 """Extract the crasher and set its permissions. 60 61 crasher is only gzipped to subvert Portage stripping. 62 63 @param root_path: Root directory of the chroot environment in which the 64 crasher is installed and run. 65 """ 66 self._root_path = root_path 67 self._crasher_path = os.path.join(self.srcdir, 'crasher', CRASHER) 68 utils.system('cd %s; tar xzf crasher.tgz-unmasked' % 69 os.path.dirname(self._crasher_path)) 70 # Make sure all users (specifically chronos) have access to 71 # this directory and its decendents in order to run crasher 72 # executable as different users. 73 utils.system('chmod -R a+rx ' + self.bindir) 74 75 76 def _populate_symbols(self): 77 """Set up Breakpad's symbol structure. 78 79 Breakpad's minidump processor expects symbols to be in a directory 80 hierarchy: 81 <symbol-root>/<module_name>/<file_id>/<module_name>.sym 82 """ 83 self._symbol_dir = os.path.join(os.path.dirname(self._crasher_path), 84 'symbols') 85 utils.system('rm -rf %s' % self._symbol_dir) 86 os.mkdir(self._symbol_dir) 87 88 basename = os.path.basename(self._crasher_path) 89 utils.system('/usr/bin/dump_syms %s > %s.sym' % 90 (self._crasher_path, 91 basename)) 92 sym_name = '%s.sym' % basename 93 symbols = utils.read_file(sym_name) 94 # First line should be like: 95 # MODULE Linux x86 7BC3323FBDBA2002601FA5BA3186D6540 crasher_XXX 96 # or 97 # MODULE Linux arm C2FE4895B203D87DD4D9227D5209F7890 crasher_XXX 98 first_line = symbols.split('\n')[0] 99 tokens = first_line.split() 100 if tokens[0] != 'MODULE' or tokens[1] != 'Linux': 101 raise error.TestError('Unexpected symbols format: %s', 102 first_line) 103 file_id = tokens[3] 104 target_dir = os.path.join(self._symbol_dir, basename, file_id) 105 os.makedirs(target_dir) 106 os.rename(sym_name, os.path.join(target_dir, sym_name)) 107 108 109 def _is_frame_in_stack(self, frame_index, module_name, 110 function_name, file_name, 111 line_number, stack): 112 """Search for frame entries in the given stack dump text. 113 114 A frame entry looks like (alone on a line): 115 16 crasher_nobreakpad!main [crasher.cc : 21 + 0xb] 116 117 Args: 118 frame_index: number of the stack frame (0 is innermost frame) 119 module_name: name of the module (executable or dso) 120 function_name: name of the function in the stack 121 file_name: name of the file containing the function 122 line_number: line number 123 stack: text string of stack frame entries on separate lines. 124 125 Returns: 126 Boolean indicating if an exact match is present. 127 128 Note: 129 We do not care about the full function signature - ie, is it 130 foo or foo(ClassA *). These are present in function names 131 pulled by dump_syms for Stabs but not for DWARF. 132 """ 133 regexp = (r'\n\s*%d\s+%s!%s.*\[\s*%s\s*:\s*%d\s.*\]' % 134 (frame_index, module_name, 135 function_name, file_name, 136 line_number)) 137 logging.info('Searching for regexp %s', regexp) 138 return re.search(regexp, stack) is not None 139 140 141 def _verify_stack(self, stack, basename, from_crash_reporter): 142 logging.debug('minidump_stackwalk output:\n%s', stack) 143 144 # Should identify cause as SIGSEGV at address 0x16 145 match = re.search(r'Crash reason:\s+(.*)', stack) 146 expected_address = '0x16' 147 if from_crash_reporter: 148 # We cannot yet determine the crash address when coming 149 # through core files via crash_reporter. 150 expected_address = '0x0' 151 if not match or match.group(1) != 'SIGSEGV': 152 raise error.TestFail('Did not identify SIGSEGV cause') 153 match = re.search(r'Crash address:\s+(.*)', stack) 154 if not match or match.group(1) != expected_address: 155 raise error.TestFail('Did not identify crash address %s' % 156 expected_address) 157 158 # Should identify crash at *(char*)0x16 assignment line 159 if not self._is_frame_in_stack(0, basename, 160 'recbomb', 'bomb.cc', 9, stack): 161 raise error.TestFail('Did not show crash line on stack') 162 163 # Should identify recursion line which is on the stack 164 # for 15 levels 165 if not self._is_frame_in_stack(15, basename, 'recbomb', 166 'bomb.cc', 12, stack): 167 raise error.TestFail('Did not show recursion line on stack') 168 169 # Should identify main line 170 if not self._is_frame_in_stack(16, basename, 'main', 171 'crasher.cc', 24, stack): 172 raise error.TestFail('Did not show main on stack') 173 174 175 def _run_crasher_process(self, username, cause_crash=True, consent=True, 176 crasher_path=None, run_crasher=None, 177 expected_uid=None, expected_exit_code=None, 178 expected_reason=None): 179 """Runs the crasher process. 180 181 Will wait up to 10 seconds for crash_reporter to report the crash. 182 crash_reporter_caught will be marked as true when the "Received crash 183 notification message..." appears. While associated logs are likely to be 184 available at this point, the function does not guarantee this. 185 186 @param username: Unix user of the crasher process. 187 @param cause_crash: Whether the crasher should crash. 188 @param consent: Whether the user consents to crash reporting. 189 @param crasher_path: Path to which the crasher should be copied before 190 execution. Relative to |_root_path|. 191 @param run_crasher: A closure to override the default |crasher_command| 192 invocation. It should return a tuple describing the 193 process, where |pid| can be None if it should be 194 parsed from the |output|: 195 196 def run_crasher(username, crasher_command): 197 ... 198 return (exit_code, output, pid) 199 200 @param expected_uid: 201 @param expected_exit_code: 202 @param expected_reason: 203 Expected information in crash_reporter log message. 204 205 @returns: 206 A dictionary with keys: 207 returncode: return code of the crasher 208 crashed: did the crasher return segv error code 209 crash_reporter_caught: did crash_reporter catch a segv 210 output: stderr output of the crasher process 211 """ 212 if crasher_path is None: 213 crasher_path = self._crasher_path 214 else: 215 dest = os.path.join(self._root_path, 216 crasher_path[os.path.isabs(crasher_path):]) 217 218 utils.system('cp -a "%s" "%s"' % (self._crasher_path, dest)) 219 220 self.enable_crash_filtering(os.path.basename(crasher_path)) 221 222 crasher_command = [] 223 224 if username == 'root': 225 if expected_exit_code is None: 226 expected_exit_code = -signal.SIGSEGV 227 else: 228 if expected_exit_code is None: 229 expected_exit_code = 128 + signal.SIGSEGV 230 231 if not run_crasher: 232 crasher_command.extend(['su', username, '-c']) 233 234 crasher_command.append(crasher_path) 235 basename = os.path.basename(crasher_path) 236 if not cause_crash: 237 crasher_command.append('--nocrash') 238 self._set_consent(consent) 239 240 logging.debug('Running crasher: %s', crasher_command) 241 242 if run_crasher: 243 (exit_code, output, pid) = run_crasher(username, crasher_command) 244 245 else: 246 crasher = subprocess.Popen(crasher_command, 247 stdout=subprocess.PIPE, 248 stderr=subprocess.PIPE) 249 250 output = crasher.communicate()[1] 251 exit_code = crasher.returncode 252 pid = None 253 254 logging.debug('Crasher output:\n%s', output) 255 256 if pid is None: 257 # Get the PID from the output, since |crasher.pid| may be su's PID. 258 match = re.search(r'pid=(\d+)', output) 259 if not match: 260 raise error.TestFail('Missing PID in crasher output') 261 pid = int(match.group(1)) 262 263 if expected_uid is None: 264 expected_uid = pwd.getpwnam(username)[2] 265 266 if expected_reason is None: 267 expected_reason = 'handling' if consent else 'ignoring - no consent' 268 269 expected_message = ( 270 '[%s] Received crash notification for %s[%d] sig 11, user %d (%s)' % 271 (self._expected_tag, basename, pid, expected_uid, expected_reason)) 272 273 # Wait until no crash_reporter is running. 274 utils.poll_for_condition( 275 lambda: utils.system('pgrep -f crash_reporter.*:%s' % basename, 276 ignore_status=True) != 0, 277 timeout=10, 278 exception=error.TestError( 279 'Timeout waiting for crash_reporter to finish: ' + 280 self._log_reader.get_logs())) 281 282 is_caught = False 283 try: 284 utils.poll_for_condition( 285 lambda: self._log_reader.can_find(expected_message), 286 timeout=5, 287 desc='Logs contain crash_reporter message: ' + expected_message) 288 is_caught = True 289 except utils.TimeoutError: 290 pass 291 292 result = {'crashed': exit_code == expected_exit_code, 293 'crash_reporter_caught': is_caught, 294 'output': output, 295 'returncode': exit_code} 296 logging.debug('Crasher process result: %s', result) 297 return result 298 299 300 def _check_crash_directory_permissions(self, crash_dir): 301 stat_info = os.stat(crash_dir) 302 user = pwd.getpwuid(stat_info.st_uid)[0] 303 group = grp.getgrgid(stat_info.st_gid)[0] 304 mode = stat.S_IMODE(stat_info.st_mode) 305 306 if crash_dir == '/var/spool/crash': 307 expected_user = 'root' 308 expected_group = 'root' 309 expected_mode = 01755 310 else: 311 expected_user = 'chronos' 312 expected_group = 'chronos' 313 expected_mode = 0755 314 315 if user != expected_user or group != expected_group: 316 raise error.TestFail( 317 'Expected %s.%s ownership of %s (actual %s.%s)' % 318 (expected_user, expected_group, crash_dir, user, group)) 319 if mode != expected_mode: 320 raise error.TestFail( 321 'Expected %s to have mode %o (actual %o)' % 322 (crash_dir, expected_mode, mode)) 323 324 325 def _check_minidump_stackwalk(self, minidump_path, basename, 326 from_crash_reporter): 327 stack = utils.system_output('/usr/bin/minidump_stackwalk %s %s' % 328 (minidump_path, self._symbol_dir)) 329 self._verify_stack(stack, basename, from_crash_reporter) 330 331 332 def _check_generated_report_sending(self, meta_path, payload_path, 333 exec_name, report_kind, 334 expected_sig=None): 335 # Now check that the sending works 336 result = self._call_sender_one_crash( 337 report=os.path.basename(payload_path)) 338 if (not result['send_attempt'] or not result['send_success'] or 339 result['report_exists']): 340 raise error.TestFail('Report not sent properly') 341 if result['exec_name'] != exec_name: 342 raise error.TestFail('Executable name incorrect') 343 if result['report_kind'] != report_kind: 344 raise error.TestFail('Expected a %s report' % report_kind) 345 if result['report_payload'] != payload_path: 346 raise error.TestFail('Sent the wrong minidump payload') 347 if result['meta_path'] != meta_path: 348 raise error.TestFail('Used the wrong meta file') 349 if expected_sig is None: 350 if result['sig'] is not None: 351 raise error.TestFail('Report should not have signature') 352 else: 353 if not 'sig' in result or result['sig'] != expected_sig: 354 raise error.TestFail('Report signature mismatch: %s vs %s' % 355 (result['sig'], expected_sig)) 356 357 version = self._expected_version 358 if version is None: 359 lsb_release = utils.read_file('/etc/lsb-release') 360 version = re.search( 361 r'CHROMEOS_RELEASE_VERSION=(.*)', lsb_release).group(1) 362 363 if not ('Version: %s' % version) in result['output']: 364 raise error.TestFail('Missing version %s in log output' % version) 365 366 367 def _run_crasher_process_and_analyze(self, username, 368 cause_crash=True, consent=True, 369 crasher_path=None, run_crasher=None, 370 expected_uid=None, 371 expected_exit_code=None): 372 self._log_reader.set_start_by_current() 373 374 result = self._run_crasher_process( 375 username, cause_crash=cause_crash, consent=consent, 376 crasher_path=crasher_path, run_crasher=run_crasher, 377 expected_uid=expected_uid, expected_exit_code=expected_exit_code) 378 379 if not result['crashed'] or not result['crash_reporter_caught']: 380 return result 381 382 crash_dir = self._get_crash_dir(username, self._force_user_crash_dir) 383 384 if not consent: 385 if os.path.exists(crash_dir): 386 raise error.TestFail('Crash directory should not exist') 387 return result 388 389 if not os.path.exists(crash_dir): 390 raise error.TestFail('Crash directory does not exist') 391 392 crash_contents = os.listdir(crash_dir) 393 basename = os.path.basename(crasher_path or self._crasher_path) 394 395 breakpad_minidump = None 396 crash_reporter_minidump = None 397 crash_reporter_meta = None 398 crash_reporter_log = None 399 400 self._check_crash_directory_permissions(crash_dir) 401 402 logging.debug('Contents in %s: %s', crash_dir, crash_contents) 403 404 for filename in crash_contents: 405 if filename.endswith('.core'): 406 # Ignore core files. We'll test them later. 407 pass 408 elif (filename.startswith(basename) and 409 filename.endswith('.dmp')): 410 # This appears to be a minidump created by the crash reporter. 411 if not crash_reporter_minidump is None: 412 raise error.TestFail('Crash reporter wrote multiple ' 413 'minidumps') 414 crash_reporter_minidump = os.path.join( 415 self._canonicalize_crash_dir(crash_dir), filename) 416 elif (filename.startswith(basename) and 417 filename.endswith('.meta')): 418 if not crash_reporter_meta is None: 419 raise error.TestFail('Crash reporter wrote multiple ' 420 'meta files') 421 crash_reporter_meta = os.path.join(crash_dir, filename) 422 elif (filename.startswith(basename) and 423 filename.endswith('.log')): 424 if not crash_reporter_log is None: 425 raise error.TestFail('Crash reporter wrote multiple ' 426 'log files') 427 crash_reporter_log = os.path.join(crash_dir, filename) 428 else: 429 # This appears to be a breakpad created minidump. 430 if not breakpad_minidump is None: 431 raise error.TestFail('Breakpad wrote multiple minidumps') 432 breakpad_minidump = os.path.join(crash_dir, filename) 433 434 if breakpad_minidump: 435 raise error.TestFail('%s did generate breakpad minidump' % basename) 436 437 if not crash_reporter_meta: 438 raise error.TestFail('crash reporter did not generate meta') 439 440 result['minidump'] = crash_reporter_minidump 441 result['basename'] = basename 442 result['meta'] = crash_reporter_meta 443 result['log'] = crash_reporter_log 444 return result 445 446 447 def _check_crashed_and_caught(self, result): 448 if not result['crashed']: 449 raise error.TestFail('Crasher returned %d instead of crashing' % 450 result['returncode']) 451 452 if not result['crash_reporter_caught']: 453 logging.debug('Logs do not contain crash_reporter message:\n%s', 454 self._log_reader.get_logs()) 455 raise error.TestFail('crash_reporter did not catch crash') 456 457 458 def _check_crashing_process(self, username, consent=True, 459 crasher_path=None, run_crasher=None, 460 expected_uid=None, expected_exit_code=None): 461 result = self._run_crasher_process_and_analyze( 462 username, consent=consent, 463 crasher_path=crasher_path, 464 run_crasher=run_crasher, 465 expected_uid=expected_uid, 466 expected_exit_code=expected_exit_code) 467 468 self._check_crashed_and_caught(result) 469 470 if not consent: 471 return 472 473 if not result['minidump']: 474 raise error.TestFail('crash reporter did not generate minidump') 475 476 if not self._log_reader.can_find('Stored minidump to ' + 477 result['minidump']): 478 raise error.TestFail('crash reporter did not announce minidump') 479 480 self._check_minidump_stackwalk(result['minidump'], 481 result['basename'], 482 from_crash_reporter=True) 483 self._check_generated_report_sending(result['meta'], 484 result['minidump'], 485 result['basename'], 486 'minidump') 487