• 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    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