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