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