brintos

brintos / llvm-project-archived public Read only

0
0
Text · 16.4 KiB · 22e3dbb Raw
485 lines · python
1#!/usr/bin/env python32"""Calls reduction tools to create minimal reproducers for clang crashes.3 4Unknown arguments are treated at cvise/creduce options.5 6Output files:7  *.reduced.sh -- crash reproducer with minimal arguments8  *.reduced.cpp -- the reduced file9  *.test.sh -- interestingness test for C-Vise10"""11 12from argparse import ArgumentParser, RawTextHelpFormatter13import os14import re15import shutil16import stat17import sys18import subprocess19import shlex20import tempfile21import shutil22import multiprocessing23 24verbose = False25creduce_cmd = None26clang_cmd = None27 28 29def verbose_print(*args, **kwargs):30    if verbose:31        print(*args, **kwargs)32 33 34def check_file(fname):35    fname = os.path.normpath(fname)36    if not os.path.isfile(fname):37        sys.exit("ERROR: %s does not exist" % (fname))38    return fname39 40 41def check_cmd(cmd_name, cmd_dir, cmd_path=None):42    """43    Returns absolute path to cmd_path if it is given,44    or absolute path to cmd_dir/cmd_name.45    """46    if cmd_path:47        # Make the path absolute so the creduce test can be run from any directory.48        cmd_path = os.path.abspath(cmd_path)49        cmd = shutil.which(cmd_path)50        if cmd:51            return cmd52        sys.exit("ERROR: executable `%s` not found" % (cmd_path))53 54    cmd = shutil.which(cmd_name, path=cmd_dir)55    if cmd:56        return cmd57 58    if not cmd_dir:59        cmd_dir = "$PATH"60    sys.exit("ERROR: `%s` not found in %s" % (cmd_name, cmd_dir))61 62 63def quote_cmd(cmd):64    return " ".join(shlex.quote(arg) for arg in cmd)65 66 67def write_to_script(text, filename):68    with open(filename, "w") as f:69        f.write(text)70    os.chmod(filename, os.stat(filename).st_mode | stat.S_IEXEC)71 72 73class Reduce(object):74    def __init__(self, crash_script, file_to_reduce, creduce_flags):75        crash_script_name, crash_script_ext = os.path.splitext(crash_script)76        file_reduce_name, file_reduce_ext = os.path.splitext(file_to_reduce)77 78        self.testfile = file_reduce_name + ".test.sh"79        self.crash_script = crash_script_name + ".reduced" + crash_script_ext80        self.file_to_reduce = file_reduce_name + ".reduced" + file_reduce_ext81        shutil.copy(file_to_reduce, self.file_to_reduce)82 83        self.clang = clang_cmd84        self.clang_args = []85        self.expected_output = []86        self.needs_stack_trace = False87        self.creduce_flags = ["--tidy"] + creduce_flags88 89        self.read_clang_args(crash_script, file_to_reduce)90        self.read_expected_output()91 92    def get_crash_cmd(self, cmd=None, args=None, filename=None):93        if not cmd:94            cmd = self.clang95        if not args:96            args = self.clang_args97        if not filename:98            filename = self.file_to_reduce99 100        return [cmd] + args + [filename]101 102    def read_clang_args(self, crash_script, filename):103        print("\nReading arguments from crash script...")104        with open(crash_script) as f:105            # Assume clang call is the first non comment line.106            cmd = []107            for line in f:108                if not line.lstrip().startswith("#"):109                    cmd = shlex.split(line)110                    break111        if not cmd:112            sys.exit("Could not find command in the crash script.")113 114        # Remove clang and filename from the command115        # Assume the last occurrence of the filename is the clang input file116        del cmd[0]117        for i in range(len(cmd) - 1, -1, -1):118            if cmd[i] == filename:119                del cmd[i]120                break121        self.clang_args = cmd122        verbose_print("Clang arguments:", quote_cmd(self.clang_args))123 124    def read_expected_output(self):125        print("\nGetting expected crash output...")126        p = subprocess.Popen(127            self.get_crash_cmd(), stdout=subprocess.PIPE, stderr=subprocess.STDOUT128        )129        crash_output, _ = p.communicate()130        result = []131 132        # Remove color codes133        ansi_escape = r"\x1b\[[0-?]*m"134        crash_output = re.sub(ansi_escape, "", crash_output.decode("utf-8"))135 136        # Look for specific error messages137        regexes = [138            r"Assertion .+ failed",  # Linux assert()139            r"Assertion failed: .+,",  # FreeBSD/Mac assert()140            r"fatal error: error in backend: .+",141            r"LLVM ERROR: .+",142            r"UNREACHABLE executed at .+?!",143            r"LLVM IR generation of declaration '.+'",144            r"Generating code for declaration '.+'",145            r"\*\*\* Bad machine code: .+ \*\*\*",146            r"ERROR: .*Sanitizer: [^ ]+ ",147        ]148        for msg_re in regexes:149            match = re.search(msg_re, crash_output)150            if match:151                msg = match.group(0)152                result = [msg]153                print("Found message:", msg)154                break155 156        # If no message was found, use the top five stack trace functions,157        # ignoring some common functions158        # Five is a somewhat arbitrary number; the goal is to get a small number159        # of identifying functions with some leeway for common functions160        if not result:161            self.needs_stack_trace = True162            stacktrace_re = r"[0-9]+\s+0[xX][0-9a-fA-F]+\s*([^(]+)\("163            filters = [164                "PrintStackTrace",165                "RunSignalHandlers",166                "CleanupOnSignal",167                "HandleCrash",168                "SignalHandler",169                "__restore_rt",170                "gsignal",171                "abort",172            ]173 174            def skip_function(func_name):175                return any(name in func_name for name in filters)176 177            matches = re.findall(stacktrace_re, crash_output)178            result = [x for x in matches if x and not skip_function(x)][:5]179            for msg in result:180                print("Found stack trace function:", msg)181 182        if not result:183            print("ERROR: no crash was found")184            print("The crash output was:\n========\n%s========" % crash_output)185            sys.exit(1)186 187        self.expected_output = result188 189    def check_expected_output(self, args=None, filename=None):190        if not args:191            args = self.clang_args192        if not filename:193            filename = self.file_to_reduce194 195        p = subprocess.Popen(196            self.get_crash_cmd(args=args, filename=filename),197            stdout=subprocess.PIPE,198            stderr=subprocess.STDOUT,199        )200        crash_output, _ = p.communicate()201        return all(msg in crash_output.decode("utf-8") for msg in self.expected_output)202 203    def write_interestingness_test(self):204        print("\nCreating the interestingness test...")205 206        # Disable symbolization if it's not required to avoid slow symbolization.207        disable_symbolization = ""208        if not self.needs_stack_trace:209            disable_symbolization = "export LLVM_DISABLE_SYMBOLIZATION=1"210 211        output = """#!/bin/bash212%s213if %s >& t.log ; then214  exit 1215fi216""" % (217            disable_symbolization,218            quote_cmd(self.get_crash_cmd()),219        )220 221        for msg in self.expected_output:222            output += "grep -F %s t.log || exit 1\n" % shlex.quote(msg)223 224        write_to_script(output, self.testfile)225        self.check_interestingness()226 227    def check_interestingness(self):228        testfile = os.path.abspath(self.testfile)229 230        # Check that the test considers the original file interesting231        returncode = subprocess.call(testfile, stdout=subprocess.DEVNULL)232        if returncode:233            sys.exit("The interestingness test does not pass for the original file.")234 235        # Check that an empty file is not interesting236        # Instead of modifying the filename in the test file, just run the command237        with tempfile.NamedTemporaryFile() as empty_file:238            is_interesting = self.check_expected_output(filename=empty_file.name)239        if is_interesting:240            sys.exit("The interestingness test passes for an empty file.")241 242    def clang_preprocess(self):243        print("\nTrying to preprocess the source file...")244        with tempfile.NamedTemporaryFile() as tmpfile:245            cmd_preprocess = self.get_crash_cmd() + ["-E", "-o", tmpfile.name]246            cmd_preprocess_no_lines = cmd_preprocess + ["-P"]247            try:248                subprocess.check_call(cmd_preprocess_no_lines)249                if self.check_expected_output(filename=tmpfile.name):250                    print("Successfully preprocessed with line markers removed")251                    shutil.copy(tmpfile.name, self.file_to_reduce)252                else:253                    subprocess.check_call(cmd_preprocess)254                    if self.check_expected_output(filename=tmpfile.name):255                        print("Successfully preprocessed without removing line markers")256                        shutil.copy(tmpfile.name, self.file_to_reduce)257                    else:258                        print(259                            "No longer crashes after preprocessing -- "260                            "using original source"261                        )262            except subprocess.CalledProcessError:263                print("Preprocessing failed")264 265    @staticmethod266    def filter_args(267        args, opts_equal=[], opts_startswith=[], opts_one_arg_startswith=[]268    ):269        result = []270        skip_next = False271        for arg in args:272            if skip_next:273                skip_next = False274                continue275            if any(arg == a for a in opts_equal):276                continue277            if any(arg.startswith(a) for a in opts_startswith):278                continue279            if any(arg.startswith(a) for a in opts_one_arg_startswith):280                skip_next = True281                continue282            result.append(arg)283        return result284 285    def try_remove_args(self, args, msg=None, extra_arg=None, **kwargs):286        new_args = self.filter_args(args, **kwargs)287 288        if extra_arg:289            if extra_arg in new_args:290                new_args.remove(extra_arg)291            new_args.append(extra_arg)292 293        if new_args != args and self.check_expected_output(args=new_args):294            if msg:295                verbose_print(msg)296            return new_args297        return args298 299    def try_remove_arg_by_index(self, args, index):300        new_args = args[:index] + args[index + 1 :]301        removed_arg = args[index]302 303        # Heuristic for grouping arguments:304        # remove next argument if it doesn't start with "-"305        if index < len(new_args) and not new_args[index].startswith("-"):306            del new_args[index]307            removed_arg += " " + args[index + 1]308 309        if self.check_expected_output(args=new_args):310            verbose_print("Removed", removed_arg)311            return new_args, index312        return args, index + 1313 314    def simplify_clang_args(self):315        """Simplify clang arguments before running C-Vise to reduce the time the316        interestingness test takes to run.317        """318        print("\nSimplifying the clang command...")319        new_args = self.clang_args320 321        # Remove the color diagnostics flag to make it easier to match error322        # text.323        new_args = self.try_remove_args(324            new_args,325            msg="Removed -fcolor-diagnostics",326            opts_equal=["-fcolor-diagnostics"],327        )328 329        # Remove some clang arguments to speed up the interestingness test330        new_args = self.try_remove_args(331            new_args,332            msg="Removed debug info options",333            opts_startswith=["-gcodeview", "-debug-info-kind=", "-debugger-tuning="],334        )335 336        new_args = self.try_remove_args(337            new_args, msg="Removed --show-includes", opts_startswith=["--show-includes"]338        )339        # Not suppressing warnings (-w) sometimes prevents the crash from occurring340        # after preprocessing341        new_args = self.try_remove_args(342            new_args,343            msg="Replaced -W options with -w",344            extra_arg="-w",345            opts_startswith=["-W"],346        )347        new_args = self.try_remove_args(348            new_args,349            msg="Replaced optimization level with -O0",350            extra_arg="-O0",351            opts_startswith=["-O"],352        )353 354        # Try to remove compilation steps355        new_args = self.try_remove_args(356            new_args, msg="Added -emit-llvm", extra_arg="-emit-llvm"357        )358        new_args = self.try_remove_args(359            new_args, msg="Added -fsyntax-only", extra_arg="-fsyntax-only"360        )361 362        # Try to make implicit int an error for more sensible test output363        new_args = self.try_remove_args(364            new_args,365            msg="Added -Werror=implicit-int",366            opts_equal=["-w"],367            extra_arg="-Werror=implicit-int",368        )369 370        self.clang_args = new_args371        verbose_print("Simplified command:", quote_cmd(self.get_crash_cmd()))372 373    def reduce_clang_args(self):374        """Minimize the clang arguments after running C-Vise, to get the smallest375        command that reproduces the crash on the reduced file.376        """377        print("\nReducing the clang crash command...")378 379        new_args = self.clang_args380 381        # Remove some often occurring args382        new_args = self.try_remove_args(383            new_args, msg="Removed -D options", opts_startswith=["-D"]384        )385        new_args = self.try_remove_args(386            new_args, msg="Removed -D options", opts_one_arg_startswith=["-D"]387        )388        new_args = self.try_remove_args(389            new_args, msg="Removed -I options", opts_startswith=["-I"]390        )391        new_args = self.try_remove_args(392            new_args, msg="Removed -I options", opts_one_arg_startswith=["-I"]393        )394        new_args = self.try_remove_args(395            new_args, msg="Removed -W options", opts_startswith=["-W"]396        )397 398        # Remove other cases that aren't covered by the heuristic399        new_args = self.try_remove_args(400            new_args, msg="Removed -mllvm", opts_one_arg_startswith=["-mllvm"]401        )402 403        i = 0404        while i < len(new_args):405            new_args, i = self.try_remove_arg_by_index(new_args, i)406 407        self.clang_args = new_args408 409        reduced_cmd = quote_cmd(self.get_crash_cmd())410        write_to_script(reduced_cmd, self.crash_script)411        print("Reduced command:", reduced_cmd)412 413    def run_creduce(self):414        full_creduce_cmd = (415            [creduce_cmd] + self.creduce_flags + [self.testfile, self.file_to_reduce]416        )417        print("\nRunning C reduction tool...")418        verbose_print(quote_cmd(full_creduce_cmd))419        try:420            p = subprocess.Popen(full_creduce_cmd)421            p.communicate()422        except KeyboardInterrupt:423            # Hack to kill C-Reduce because it jumps into its own pgid424            print("\n\nctrl-c detected, killed reduction tool")425            p.kill()426 427 428def main():429    global verbose430    global creduce_cmd431    global clang_cmd432 433    parser = ArgumentParser(description=__doc__, formatter_class=RawTextHelpFormatter)434    parser.add_argument(435        "crash_script",436        type=str,437        nargs=1,438        help="Name of the script that generates the crash.",439    )440    parser.add_argument(441        "file_to_reduce", type=str, nargs=1, help="Name of the file to be reduced."442    )443    parser.add_argument(444        "--llvm-bin", dest="llvm_bin", type=str, help="Path to the LLVM bin directory."445    )446    parser.add_argument(447        "--clang",448        dest="clang",449        type=str,450        help="The path to the `clang` executable. "451        "By default uses the llvm-bin directory.",452    )453    parser.add_argument(454        "--creduce",455        dest="creduce",456        type=str,457        help="The path to the `creduce` or `cvise` executable. "458        "Required if neither `creduce` nor `cvise` are on PATH.",459    )460    parser.add_argument("-v", "--verbose", action="store_true")461    args, creduce_flags = parser.parse_known_args()462    verbose = args.verbose463    llvm_bin = os.path.abspath(args.llvm_bin) if args.llvm_bin else None464    creduce_cmd = check_cmd("creduce", None, args.creduce)465    creduce_cmd = check_cmd("cvise", None, args.creduce)466    clang_cmd = check_cmd("clang", llvm_bin, args.clang)467 468    crash_script = check_file(args.crash_script[0])469    file_to_reduce = check_file(args.file_to_reduce[0])470 471    if "--n" not in creduce_flags:472        creduce_flags += ["--n", str(max(4, multiprocessing.cpu_count() // 2))]473 474    r = Reduce(crash_script, file_to_reduce, creduce_flags)475 476    r.simplify_clang_args()477    r.write_interestingness_test()478    r.clang_preprocess()479    r.run_creduce()480    r.reduce_clang_args()481 482 483if __name__ == "__main__":484    main()485