Home | History | Annotate | Download | only in crash
      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 
      5 import grp
      6 import logging
      7 import os
      8 import pwd
      9 import re
     10 import shutil
     11 import signal
     12 import stat
     13 import subprocess
     14 
     15 import crash_test
     16 from autotest_lib.client.bin import utils
     17 from autotest_lib.client.common_lib import error
     18 
     19 
     20 CRASHER = 'crasher_nobreakpad'
     21 
     22 
     23 class 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', 23, 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