|  | # Copyright (c) 2014 The Chromium Authors. All rights reserved. | 
|  | # Use of this source code is governed by a BSD-style license that can be | 
|  | # found in the LICENSE file. | 
|  |  | 
|  | import re | 
|  |  | 
|  | import crash_utils | 
|  |  | 
|  |  | 
|  | SYZYASAN_STACK_FRAME_PATTERN = re.compile( | 
|  | r'(CF: )?(.*?)( \(FPO: .*\) )?( \(CONV: .*\) )?\[(.*) @ (\d+)\]') | 
|  | FILE_PATH_AND_LINE_PATTERN = re.compile(r'(.*?):(\d+)(:\d+)?') | 
|  |  | 
|  |  | 
|  | class StackFrame(object): | 
|  | """Represents a frame in stacktrace. | 
|  |  | 
|  | Attributes: | 
|  | index: An index of the stack frame. | 
|  | component_path: The path of the component this frame represents. | 
|  | component_name: The name of the component this frame represents. | 
|  | file_name: The name of the file that crashed. | 
|  | function: The function that caused the crash. | 
|  | file_path: The path of the crashed file. | 
|  | crashed_line_range: The line of the file that caused the crash. | 
|  | """ | 
|  |  | 
|  | def __init__(self, stack_frame_index, component_path, component_name, | 
|  | file_name, function, file_path, crashed_line_range): | 
|  | self.index = stack_frame_index | 
|  | self.component_path = component_path | 
|  | self.component_name = component_name | 
|  | self.file_name = file_name | 
|  | self.function = function | 
|  | self.file_path = file_path | 
|  | self.crashed_line_range = crashed_line_range | 
|  |  | 
|  |  | 
|  | class CallStack(object): | 
|  | """Represents a call stack within a stacktrace. | 
|  |  | 
|  | It is a list of StackFrame object, and the object keeps track of whether | 
|  | the stack is crash stack, freed or previously-allocated. | 
|  | """ | 
|  |  | 
|  | def __init__(self, stack_priority): | 
|  | self.frame_list = [] | 
|  | self.priority = stack_priority | 
|  |  | 
|  | def Add(self, stacktrace_line): | 
|  | self.frame_list.append(stacktrace_line) | 
|  |  | 
|  | def GetTopNFrames(self, n): | 
|  | return self.frame_list[:n] | 
|  |  | 
|  |  | 
|  | class Stacktrace(object): | 
|  | """Represents Stacktrace object. | 
|  |  | 
|  | Contains a list of callstacks, because one stacktrace might have more than | 
|  | one callstacks. | 
|  | """ | 
|  |  | 
|  | def __init__(self, stacktrace, build_type, parsed_deps): | 
|  | self.stack_list = None | 
|  | self.ParseStacktrace(stacktrace, build_type, parsed_deps) | 
|  |  | 
|  | def ParseStacktrace(self, stacktrace, build_type, parsed_deps): | 
|  | """Parses stacktrace and normalizes it. | 
|  |  | 
|  | If there are multiple callstacks within the stacktrace, | 
|  | it will parse each of them separately, and store them in the stack_list | 
|  | variable. | 
|  |  | 
|  | Args: | 
|  | stacktrace: A string containing stacktrace. | 
|  | build_type: A string containing the build type of the crash. | 
|  | parsed_deps: A parsed DEPS file to normalize path with. | 
|  | """ | 
|  | # If the passed in string is empty, the object does not represent anything. | 
|  | if not stacktrace: | 
|  | return | 
|  | # Reset the stack list. | 
|  | self.stack_list = [] | 
|  | reached_new_callstack = False | 
|  | # Note that we do not need exact stack frame index, we only need relative | 
|  | # position of a frame within a callstack. The reason for not extracting | 
|  | # index from a line is that some stack frames do not have index. | 
|  | stack_frame_index = 0 | 
|  | current_stack = CallStack(-1) | 
|  |  | 
|  | for line in stacktrace: | 
|  | line = line.strip() | 
|  | (is_new_callstack, stack_priority) = self.__IsStartOfNewCallStack( | 
|  | line, build_type) | 
|  | if is_new_callstack: | 
|  | # If this callstack is crash stack, update the boolean. | 
|  | if not reached_new_callstack: | 
|  | reached_new_callstack = True | 
|  | current_stack = CallStack(stack_priority) | 
|  |  | 
|  | # If this is from freed or allocation, add the callstack we have | 
|  | # to the list of callstacks, and increment the stack priority. | 
|  | else: | 
|  | stack_frame_index = 0 | 
|  | if current_stack and current_stack.frame_list: | 
|  | self.stack_list.append(current_stack) | 
|  | current_stack = CallStack(stack_priority) | 
|  |  | 
|  | # Generate stack frame object from the line. | 
|  | parsed_stack_frame = self.__GenerateStackFrame( | 
|  | stack_frame_index, line, build_type, parsed_deps) | 
|  |  | 
|  | # If the line does not represent the stack frame, ignore this line. | 
|  | if not parsed_stack_frame: | 
|  | continue | 
|  |  | 
|  | # Add the parsed stack frame object to the current stack. | 
|  | current_stack.Add(parsed_stack_frame) | 
|  | stack_frame_index += 1 | 
|  |  | 
|  | # Add the current callstack only if there are frames in it. | 
|  | if current_stack and current_stack.frame_list: | 
|  | self.stack_list.append(current_stack) | 
|  |  | 
|  | def __IsStartOfNewCallStack(self, line, build_type): | 
|  | """Check if this line is the start of the new callstack. | 
|  |  | 
|  | Since each builds have different format of stacktrace, the logic for | 
|  | checking the line for all builds is handled in here. | 
|  |  | 
|  | Args: | 
|  | line: Line to check for. | 
|  | build_type: The name of the build. | 
|  |  | 
|  | Returns: | 
|  | True if the line is the start of new callstack, False otherwise. If True, | 
|  | it also returns the priority of the line. | 
|  | """ | 
|  | if 'syzyasan' in build_type: | 
|  | # In syzyasan build, new stack starts with 'crash stack:', | 
|  | # 'freed stack:', etc. | 
|  | callstack_start_pattern = re.compile(r'^(.*) stack:$') | 
|  | match = callstack_start_pattern.match(line) | 
|  |  | 
|  | # If the line matches the callstack start pattern. | 
|  | if match: | 
|  | # Check the type of the new match. | 
|  | stack_type = match.group(1) | 
|  |  | 
|  | # Crash stack gets priority 0. | 
|  | if stack_type == 'Crash': | 
|  | return (True, 0) | 
|  |  | 
|  | # Other callstacks all get priority 1. | 
|  | else: | 
|  | return (True, 1) | 
|  |  | 
|  | elif 'tsan' in build_type: | 
|  | # Create patterns for each callstack type. | 
|  | crash_callstack_start_pattern1 = re.compile( | 
|  | r'^(Read|Write) of size \d+') | 
|  |  | 
|  | crash_callstack_start_pattern2 = re.compile( | 
|  | r'^[A-Z]+: ThreadSanitizer') | 
|  |  | 
|  | allocation_callstack_start_pattern = re.compile( | 
|  | r'^Previous (write|read) of size \d+') | 
|  |  | 
|  | location_callstack_start_pattern = re.compile( | 
|  | r'^Location is heap block of size \d+') | 
|  |  | 
|  | # Crash stack gets priority 0. | 
|  | if (crash_callstack_start_pattern1.match(line) or | 
|  | crash_callstack_start_pattern2.match(line)): | 
|  | return (True, 0) | 
|  |  | 
|  | # All other stacks get priority 1. | 
|  | if allocation_callstack_start_pattern.match(line): | 
|  | return (True, 1) | 
|  |  | 
|  | if location_callstack_start_pattern.match(line): | 
|  | return (True, 1) | 
|  |  | 
|  | else: | 
|  | # In asan and other build types, crash stack can start | 
|  | # in two different ways. | 
|  | crash_callstack_start_pattern1 = re.compile(r'^==\d+== ?[A-Z]+:') | 
|  | crash_callstack_start_pattern2 = re.compile( | 
|  | r'^(READ|WRITE) of size \d+ at') | 
|  | crash_callstack_start_pattern3 = re.compile(r'^backtrace:') | 
|  |  | 
|  | freed_callstack_start_pattern = re.compile( | 
|  | r'^freed by thread T\d+ (.* )?here:') | 
|  |  | 
|  | allocation_callstack_start_pattern = re.compile( | 
|  | r'^previously allocated by thread T\d+ (.* )?here:') | 
|  |  | 
|  | other_callstack_start_pattern = re.compile( | 
|  | r'^Thread T\d+ (.* )?created by') | 
|  |  | 
|  | # Crash stack gets priority 0. | 
|  | if (crash_callstack_start_pattern1.match(line) or | 
|  | crash_callstack_start_pattern2.match(line) or | 
|  | crash_callstack_start_pattern3.match(line)): | 
|  | return (True, 0) | 
|  |  | 
|  | # All other callstack gets priority 1. | 
|  | if freed_callstack_start_pattern.match(line): | 
|  | return (True, 1) | 
|  |  | 
|  | if allocation_callstack_start_pattern.match(line): | 
|  | return (True, 1) | 
|  |  | 
|  | if other_callstack_start_pattern.match(line): | 
|  | return (True, 1) | 
|  |  | 
|  | # If the line does not match any pattern, return false and a dummy for | 
|  | # stack priority. | 
|  | return (False, -1) | 
|  |  | 
|  | def __GenerateStackFrame(self, stack_frame_index, line, build_type, | 
|  | parsed_deps): | 
|  | """Extracts information from a line in stacktrace. | 
|  |  | 
|  | Args: | 
|  | stack_frame_index: A stack frame index of this line. | 
|  | line: A stacktrace string to extract data from. | 
|  | build_type: A string containing the build type | 
|  | of this crash (e.g. linux_asan_chrome_mp). | 
|  | parsed_deps: A parsed DEPS file to normalize path with. | 
|  |  | 
|  | Returns: | 
|  | A triple containing the name of the function, the path of the file and | 
|  | the crashed line number. | 
|  | """ | 
|  | line_parts = line.split() | 
|  | try: | 
|  |  | 
|  | if 'syzyasan' in build_type: | 
|  | stack_frame_match = SYZYASAN_STACK_FRAME_PATTERN.match(line) | 
|  |  | 
|  | if not stack_frame_match: | 
|  | return None | 
|  | file_path = stack_frame_match.group(5) | 
|  | crashed_line_range = [int(stack_frame_match.group(6))] | 
|  | function = stack_frame_match.group(2) | 
|  |  | 
|  | else: | 
|  | if not line_parts[0].startswith('#'): | 
|  | return None | 
|  |  | 
|  | if 'tsan' in build_type: | 
|  | file_path_and_line = line_parts[-2] | 
|  | function = ' '.join(line_parts[1:-2]) | 
|  | else: | 
|  | file_path_and_line = line_parts[-1] | 
|  | function = ' '.join(line_parts[3:-1]) | 
|  |  | 
|  | # Get file path and line info from the line. | 
|  | file_path_and_line_match = FILE_PATH_AND_LINE_PATTERN.match( | 
|  | file_path_and_line) | 
|  |  | 
|  | # Return None if the file path information is not available | 
|  | if not file_path_and_line_match: | 
|  | return None | 
|  |  | 
|  | file_path = file_path_and_line_match.group(1) | 
|  |  | 
|  | # Get the crashed line range. For example, file_path:line_number:range. | 
|  | crashed_line_range_num = file_path_and_line_match.group(3) | 
|  |  | 
|  | if crashed_line_range_num: | 
|  | # Strip ':' prefix. | 
|  | crashed_line_range_num = int(crashed_line_range_num[1:]) | 
|  | else: | 
|  | crashed_line_range_num = 0 | 
|  |  | 
|  | crashed_line_number = int(file_path_and_line_match.group(2)) | 
|  | # For example, 655:1 has crashed lines 655 and 656. | 
|  | crashed_line_range = \ | 
|  | range(crashed_line_number, | 
|  | crashed_line_number + crashed_line_range_num + 1) | 
|  |  | 
|  | # Return None if the line is malformed. | 
|  | except IndexError: | 
|  | return None | 
|  | except ValueError: | 
|  | return None | 
|  |  | 
|  | # Normalize the file path so that it can be compared to repository path. | 
|  | (component_path, component_name, file_path) = ( | 
|  | crash_utils.NormalizePath(file_path, parsed_deps)) | 
|  |  | 
|  | # Return a new stack frame object with the parsed information. | 
|  | file_name = file_path.split('/')[-1] | 
|  |  | 
|  | # If we have the common stack frame index pattern, then use it | 
|  | # since it is more reliable. | 
|  | index_match = re.match('\s*#(\d+)\s.*', line) | 
|  | if index_match: | 
|  | stack_frame_index = int(index_match.group(1)) | 
|  |  | 
|  | return StackFrame(stack_frame_index, component_path, component_name, | 
|  | file_name, function, file_path, crashed_line_range) | 
|  |  | 
|  | def __getitem__(self, index): | 
|  | return self.stack_list[index] | 
|  |  | 
|  | def GetCrashStack(self): | 
|  | """Returns the callstack with the highest priority. | 
|  |  | 
|  | Crash stack has priority 0, and allocation/freed/other thread stacks | 
|  | get priority 1. | 
|  |  | 
|  | Returns: | 
|  | The highest priority callstack in the stacktrace. | 
|  | """ | 
|  | sorted_stacklist = sorted(self.stack_list, | 
|  | key=lambda callstack: callstack.priority) | 
|  | return sorted_stacklist[0] |