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