def interesting(cli_args, temp_prefix): # pylint: disable=missing-docstring,missing-return-doc,missing-return-type-doc (regex_enabled, crash_sig, timeout, args) = parse_options(cli_args) # Examine stack for crash signature, this is needed if crash_sig is specified. runinfo = timed_run.timed_run(args, timeout, temp_prefix) if runinfo.sta == timed_run.CRASHED: sps.grabCrashLog(args[0], runinfo.pid, temp_prefix, True) time_str = " (%.3f seconds)" % runinfo.elapsedtime crash_log = temp_prefix + "-crash.txt" if runinfo.sta == timed_run.CRASHED: if os.path.exists(crash_log): # When using this script, remember to escape characters, e.g. "\(" instead of "(" ! found, _found_sig = file_contains(crash_log, crash_sig, regex_enabled) if found: print("Exit status: %s%s" % (runinfo.msg, time_str)) return True print("[Uninteresting] It crashed somewhere else!" + time_str) return False print( "[Uninteresting] It appeared to crash, but no crash log was found?" + time_str) return False print("[Uninteresting] It didn't crash." + time_str) return False
def interesting(cliArgs, tempPrefix): (regexEnabled, crashSig, timeout, args) = parseOptions(cliArgs) # Examine stack for crash signature, this is needed if crashSig is specified. runinfo = timed_run.timed_run(args, timeout, tempPrefix) if runinfo.sta == timed_run.CRASHED: sps.grabCrashLog(args[0], runinfo.pid, tempPrefix, True) timeString = " (%.3f seconds)" % runinfo.elapsedtime crashLogName = tempPrefix + "-crash.txt" if runinfo.sta == timed_run.CRASHED: if os.path.exists(crashLogName): # When using this script, remember to escape characters, e.g. "\(" instead of "(" ! found, _foundSig = utils.file_contains(crashLogName, crashSig, regexEnabled) if found: print 'Exit status: ' + runinfo.msg + timeString return True else: print "[Uninteresting] It crashed somewhere else!" + timeString return False else: print "[Uninteresting] It appeared to crash, but no crash log was found?" + timeString return False else: print "[Uninteresting] It didn't crash." + timeString return False
def interesting(args, tempPrefix): timeout = int(args[0]) returncode = subprocess.call([jsshell, "-c", args[1]], stdout=subprocess.PIPE, stderr=subprocess.PIPE) if returncode != 0: print "JS didn't compile, skipping browser test" return False wantStack = False # We do not care about the stack when using this interestingness test. runinfo = timed_run.timed_run(args[2:], timeout, tempPrefix, wantStack) print "Exit status: %s (%.3f seconds)" % (runinfo.msg, runinfo.elapsedtime) return runinfo.sta == timed_run.CRASHED or runinfo.sta == timed_run.ABNORMAL
def __init__(self, options, runthis, logPrefix, in_compare_jit, env=None): # pylint: disable=too-complex # pylint: disable=too-many-arguments,too-many-branches,too-many-locals,too-many-statements # If Lithium uses this as an interestingness test, logPrefix is likely not a Path object, so make it one. logPrefix = Path(logPrefix) pathToBinary = runthis[0].expanduser().resolve() # pylint: disable=invalid-name # This relies on the shell being a local one from compile_shell: # Ignore trailing ".exe" in Win, also abspath makes it work w/relative paths like "./js" # pylint: disable=invalid-name assert pathToBinary.with_suffix(".fuzzmanagerconf").is_file() pc = ProgramConfiguration.fromBinary( str(pathToBinary.parent / pathToBinary.stem)) pc.addProgramArguments(runthis[1:-1]) if options.valgrind: runthis = (inspect_shell.constructVgCmdList( errorCode=VALGRIND_ERROR_EXIT_CODE) + [ f"--suppressions={filename}" for filename in "valgrind_suppressions.txt" ] + runthis) timed_run_kw = {"env": (env or deepcopy(os.environ))} # Enable LSan which is enabled with non-ARM64 simulator ASan, only on Linux if platform.system( ) == "Linux" and inspect_shell.queryBuildConfiguration( options.jsengine, "asan"): env_asan_options = "detect_leaks=1," env_lsan_options = "max_leaks=1," if inspect_shell.queryBuildConfiguration(options.jsengine, "arm64-simulator"): env_asan_options = "" env_lsan_options = "" timed_run_kw["env"].update({"ASAN_OPTIONS": env_asan_options}) timed_run_kw["env"].update({"LSAN_OPTIONS": env_lsan_options}) elif not platform.system() == "Windows": timed_run_kw["preexec_fn"] = set_ulimit pc.addEnvironmentVariables(dict(timed_run_kw["env"])) lithium_logPrefix = str(logPrefix).encode("utf-8") if isinstance(lithium_logPrefix, b"".__class__): lithium_logPrefix = lithium_logPrefix.decode("utf-8", errors="replace") # logPrefix should be a string for timed_run in Lithium version 0.2.1 to work properly, apparently runinfo = timedrun.timed_run( [str(x) for x in runthis ], # Convert all Paths/bytes to strings for Lithium options.timeout, lithium_logPrefix, **timed_run_kw) lev = JS_FINE is_oom = False issues = [] auxCrashData = [] # pylint: disable=invalid-name # FuzzManager expects a list of strings rather than an iterable, so bite the # bullet and "readlines" everything into memory. # Collector adds newlines later, see https://git.io/fjoMB out_log = (logPrefix.parent / f"{logPrefix.stem}-out").with_suffix(".txt") with io.open(str(out_log), "r", encoding="utf-8", errors="replace") as f: out = [line.rstrip() for line in f] err_log = (logPrefix.parent / f"{logPrefix.stem}-err").with_suffix(".txt") with io.open(str(err_log), "r", encoding="utf-8", errors="replace") as f: err = [line.rstrip() for line in f] for line in reversed(err): if "[unhandlable oom]" in line: print("Ignoring unhandlable oom...") is_oom = True break if is_oom: lev = JS_FINE crash_log = (logPrefix.parent / f"{logPrefix.stem}-crash").with_suffix(".txt") core_file = logPrefix.parent / f"{logPrefix.stem}-core" if crash_log.is_file(): crash_log.unlink() if core_file.is_file(): core_file.unlink() dbggr_cmd = os_ops.make_dbg_cmd(runthis[0], runinfo.pid) if dbggr_cmd: core_file = Path(dbggr_cmd[-1]) if core_file.is_file(): core_file.unlink() elif options.valgrind and runinfo.return_code == VALGRIND_ERROR_EXIT_CODE: issues.append("valgrind reported an error") lev = max(lev, JS_VG_AMISS) valgrindErrorPrefix = f"=={runinfo.pid}==" for line in err: if valgrindErrorPrefix and line.startswith( valgrindErrorPrefix): issues.append(line.rstrip()) elif runinfo.sta == timedrun.CRASHED: if os_ops.grab_crash_log(runthis[0], runinfo.pid, logPrefix, True): crash_log = (logPrefix.parent / f"{logPrefix.stem}-crash").with_suffix(".txt") with io.open(str(crash_log), "r", encoding="utf-8", errors="replace") as f: auxCrashData = [line.strip() for line in f.readlines()] elif file_manipulation.amiss(logPrefix): issues.append("malloc error") lev = max(lev, JS_NEW_ASSERT_OR_CRASH) elif runinfo.return_code == 0 and not in_compare_jit: # We might have(??) run jsfunfuzz directly, so check for special kinds of bugs for line in out: if line.startswith("Found a bug: ") and not ("NestTest" in line and oomed(err)): lev = JS_DECIDED_TO_EXIT issues.append(line.rstrip()) if options.shellIsDeterministic and not understoodJsfunfuzzExit( out, err) and not oomed(err): issues.append("jsfunfuzz didn't finish") lev = JS_DID_NOT_FINISH # Copy non-crash issues to where FuzzManager's "AssertionHelper" can see it. if lev != JS_FINE: for issue in issues: err.append(f"[Non-crash bug] {issue}") activated = False # Turn on when trying to report *reliable* testcases that do not have a coredump # On Linux, fall back to run testcase via gdb using --args if core file data is unavailable # Note that this second round of running uses a different fuzzSeed as the initial if default jsfunfuzz is run # We should separate this out, i.e. running jsfunfuzz within a debugger, only if core dumps cannot be generated if (activated and platform.system() == "Linux" and shutil.which("gdb") and not auxCrashData and not in_compare_jit): print( "Note: No core file found on Linux - falling back to run via gdb" ) extracted_gdb_cmds = ["-ex", "run"] with io.open(str( Path(__file__).parent.parent / "util" / "gdb_cmds.txt"), "r", encoding="utf-8", errors="replace") as f: for line in f: if line.rstrip() and not line.startswith( "#") and not line.startswith("echo"): extracted_gdb_cmds.append("-ex") extracted_gdb_cmds.append(f"{line.rstrip()}") no_main_log_gdb_log = subprocess.run( (["gdb", "-n", "-batch"] + extracted_gdb_cmds + ["--args"] + [str(x) for x in runthis]), check=True, stderr=subprocess.PIPE, stdout=subprocess.PIPE, ) auxCrashData = no_main_log_gdb_log.stdout # Finally, make a CrashInfo object and parse stack traces for asan/crash/assertion bugs crashInfo = Crash_Info.CrashInfo.fromRawCrashData( out, err, pc, auxCrashData=auxCrashData) create_collector.printCrashInfo(crashInfo) # We only care about crashes and assertion failures on shells with no symbols # Note that looking out for the Assertion failure message is highly SpiderMonkey-specific if not is_oom and (not isinstance(crashInfo, Crash_Info.NoCrashInfo) or "Assertion failure: " in str(crashInfo.rawStderr) or "Segmentation fault" in str(crashInfo.rawStderr) or "Bus error" in str(crashInfo.rawStderr)): lev = max(lev, JS_NEW_ASSERT_OR_CRASH) try: match = options.collector.search(crashInfo) if match[0] is not None: create_collector.printMatchingSignature(match) if match[1].get("frequent"): print("Ignoring frequent bucket") lev = JS_FINE except UnicodeDecodeError: # Sometimes FM throws due to unicode issues print( "Note: FuzzManager is throwing a UnicodeDecodeError, signature matching skipped" ) match = False print( f"{logPrefix} | {summaryString(issues, lev, runinfo.elapsedtime)}") if lev != JS_FINE: summary_log = (logPrefix.parent / f"{logPrefix.stem}-summary").with_suffix(".txt") with io.open(str(summary_log), "w", encoding="utf-8", errors="replace") as f: f.writelines([ f"Number: {logPrefix}\n", f'Command: {" ".join(quote(str(x)) for x in runthis)}\n' ] + [f"Status: {i}\n" for i in issues]) self.lev = lev self.out = out self.err = err self.issues = issues self.crashInfo = crashInfo # pylint: disable=invalid-name self.match = match self.runinfo = runinfo self.return_code = runinfo.return_code
def interesting(cli_args, temp_prefix): """Interesting if the binary crashes with a possibly-desired signature on the stack. Args: cli_args (list): List of input arguments. temp_prefix (str): Temporary directory prefix, e.g. tmp1/1 or tmp4/1 Returns: bool: True if the intended signature shows up on the stack, False otherwise. """ parser = argparse.ArgumentParser( prog="crashesat", usage= "python3 -m lithium %(prog)s [options] binary [flags] testcase.ext") parser.add_argument( "-r", "--regex", action="store_true", default=False, help="Allow search for regular expressions instead of strings.") parser.add_argument( "-s", "--sig", default="", type=str, help="Match this crash signature. Defaults to '%default'.") parser.add_argument( "-t", "--timeout", default=120, type=int, help="Optionally set the timeout. Defaults to '%default' seconds.") parser.add_argument("cmd_with_flags", nargs=argparse.REMAINDER) args = parser.parse_args(cli_args) log = logging.getLogger(__name__) # Examine stack for crash signature, this is needed if args.sig is specified. runinfo = timedrun.timed_run(args.cmd_with_flags, args.timeout, temp_prefix) if runinfo.sta == timedrun.CRASHED: os_ops.grab_crash_log(Path(args.cmd_with_flags[0]), runinfo.pid, Path(temp_prefix), True) crash_log = Path(f"{temp_prefix}-crash.txt") time_str = f" ({runinfo.elapsedtime:.3f} seconds)" if runinfo.sta == timedrun.CRASHED: if crash_log.resolve().is_file(): # When using this script, remember to escape characters, e.g. "\(" instead of "(" ! if file_contains(str(crash_log), args.sig.encode("utf-8"), args.regex)[0]: log.info("Exit status: %s%s", runinfo.msg, time_str) return True log.info("[Uninteresting] It crashed somewhere else!%s", time_str) return False log.info( "[Uninteresting] It appeared to crash, but no crash log was found?%s", time_str) return False log.info("[Uninteresting] It didn't crash.%s", time_str) return False
def __init__(self, options, runthis, logPrefix, inCompareJIT): pathToBinary = runthis[0] # This relies on the shell being a local one from compileShell.py: # Ignore trailing ".exe" in Win, also abspath makes it work w/relative paths like './js' pc = ProgramConfiguration.fromBinary( os.path.abspath(pathToBinary).split('.')[0]) pc.addProgramArguments(runthis[1:-1]) if options.valgrind: runthis = (inspectShell.constructVgCmdList( errorCode=VALGRIND_ERROR_EXIT_CODE) + valgrindSuppressions(options.knownPath) + runthis) preexec_fn = ulimitSet if os.name == 'posix' else None runinfo = timed_run.timed_run(runthis, options.timeout, logPrefix, preexec_fn=preexec_fn) lev = JS_FINE issues = [] auxCrashData = [] # FuzzManager expects a list of strings rather than an iterable, so bite the # bullet and 'readlines' everything into memory. with open(logPrefix + "-out.txt") as f: out = f.readlines() with open(logPrefix + "-err.txt") as f: err = f.readlines() if options.valgrind and runinfo.return_code == VALGRIND_ERROR_EXIT_CODE: issues.append("valgrind reported an error") lev = max(lev, JS_VG_AMISS) valgrindErrorPrefix = "==" + str(runinfo.pid) + "==" for line in err: if valgrindErrorPrefix and line.startswith( valgrindErrorPrefix): issues.append(line.rstrip()) elif runinfo.sta == timed_run.CRASHED: if sps.grabCrashLog(runthis[0], runinfo.pid, logPrefix, True): with open(logPrefix + "-crash.txt") as f: auxCrashData = [line.strip() for line in f.readlines()] elif detect_malloc_errors.amiss(logPrefix): issues.append("malloc error") lev = max(lev, JS_NEW_ASSERT_OR_CRASH) elif runinfo.return_code == 0 and not inCompareJIT: # We might have(??) run jsfunfuzz directly, so check for special kinds of bugs for line in out: if line.startswith("Found a bug: ") and not ("NestTest" in line and oomed(err)): lev = JS_DECIDED_TO_EXIT issues.append(line.rstrip()) if options.shellIsDeterministic and not understoodJsfunfuzzExit( out, err) and not oomed(err): issues.append("jsfunfuzz didn't finish") lev = JS_DID_NOT_FINISH # Copy non-crash issues to where FuzzManager's "AssertionHelper.py" can see it. if lev != JS_FINE: for issue in issues: err.append("[Non-crash bug] " + issue) # Finally, make a CrashInfo object and parse stack traces for asan/crash/assertion bugs crashInfo = CrashInfo.CrashInfo.fromRawCrashData( out, err, pc, auxCrashData=auxCrashData) createCollector.printCrashInfo(crashInfo) # We only care about crashes and assertion failures on shells with no symbols # Note that looking out for the Assertion failure message is highly SpiderMonkey-specific if not isinstance(crashInfo, CrashInfo.NoCrashInfo) or \ 'Assertion failure: ' in str(crashInfo.rawStderr) or \ 'Segmentation fault' in str(crashInfo.rawStderr) or \ 'Bus error' in str(crashInfo.rawStderr): lev = max(lev, JS_NEW_ASSERT_OR_CRASH) match = options.collector.search(crashInfo) if match[0] is not None: createCollector.printMatchingSignature(match) lev = JS_FINE print("%s | %s" % (logPrefix, summaryString(issues, lev, runinfo.elapsedtime))) if lev != JS_FINE: fileManipulation.writeLinesToFile([ 'Number: ' + logPrefix + '\n', 'Command: ' + sps.shellify(runthis) + '\n' ] + ['Status: ' + i + "\n" for i in issues], logPrefix + '-summary.txt') self.lev = lev self.out = out self.err = err self.issues = issues self.crashInfo = crashInfo self.match = match self.runinfo = runinfo self.return_code = runinfo.return_code
def __init__(self, options, runthis, logPrefix, in_compare_jit, env=None): # pylint: disable=too-complex # pylint: disable=too-many-arguments,too-many-branches,too-many-locals,too-many-statements # If Lithium uses this as an interestingness test, logPrefix is likely not a Path object, so make it one. logPrefix = Path(logPrefix) pathToBinary = runthis[0].expanduser().resolve() # pylint: disable=invalid-name # This relies on the shell being a local one from compile_shell: # Ignore trailing ".exe" in Win, also abspath makes it work w/relative paths like "./js" # pylint: disable=invalid-name assert pathToBinary.with_suffix(".fuzzmanagerconf").is_file() pc = ProgramConfiguration.fromBinary( str(pathToBinary.parent / pathToBinary.stem)) pc.addProgramArguments(runthis[1:-1]) if options.valgrind: runthis = (inspect_shell.constructVgCmdList( errorCode=VALGRIND_ERROR_EXIT_CODE) + valgrindSuppressions() + runthis) timed_run_kw = {} timed_run_kw["env"] = (env or os.environ) if not platform.system() == "Windows": timed_run_kw["preexec_fn"] = set_ulimit lithium_logPrefix = str(logPrefix).encode("utf-8") # Total hack to make Python 2/3 work with Lithium if sys.version_info.major == 3 and isinstance(lithium_logPrefix, b"".__class__): # pylint: disable=redefined-variable-type lithium_logPrefix = lithium_logPrefix.decode("utf-8", errors="replace") # logPrefix should be a string for timed_run in Lithium version 0.2.1 to work properly, apparently runinfo = timed_run.timed_run( [str(x) for x in runthis ], # Convert all Paths/bytes to strings for Lithium options.timeout, lithium_logPrefix, **timed_run_kw) lev = JS_FINE issues = [] auxCrashData = [] # pylint: disable=invalid-name # FuzzManager expects a list of strings rather than an iterable, so bite the # bullet and "readlines" everything into memory. out_log = (logPrefix.parent / (logPrefix.stem + "-out")).with_suffix(".txt") with io.open(str(out_log), "r", encoding="utf-8", errors="replace") as f: out = f.readlines() err_log = (logPrefix.parent / (logPrefix.stem + "-err")).with_suffix(".txt") with io.open(str(err_log), "r", encoding="utf-8", errors="replace") as f: err = f.readlines() if options.valgrind and runinfo.return_code == VALGRIND_ERROR_EXIT_CODE: issues.append("valgrind reported an error") lev = max(lev, JS_VG_AMISS) valgrindErrorPrefix = "==" + str(runinfo.pid) + "==" for line in err: if valgrindErrorPrefix and line.startswith( valgrindErrorPrefix): issues.append(line.rstrip()) elif runinfo.sta == timed_run.CRASHED: if os_ops.grab_crash_log(runthis[0], runinfo.pid, logPrefix, True): crash_log = (logPrefix.parent / (logPrefix.stem + "-crash")).with_suffix(".txt") with io.open(str(crash_log), "r", encoding="utf-8", errors="replace") as f: auxCrashData = [line.strip() for line in f.readlines()] elif file_manipulation.amiss(logPrefix): issues.append("malloc error") lev = max(lev, JS_NEW_ASSERT_OR_CRASH) elif runinfo.return_code == 0 and not in_compare_jit: # We might have(??) run jsfunfuzz directly, so check for special kinds of bugs for line in out: if line.startswith("Found a bug: ") and not ("NestTest" in line and oomed(err)): lev = JS_DECIDED_TO_EXIT issues.append(line.rstrip()) if options.shellIsDeterministic and not understoodJsfunfuzzExit( out, err) and not oomed(err): issues.append("jsfunfuzz didn't finish") lev = JS_DID_NOT_FINISH # Copy non-crash issues to where FuzzManager's "AssertionHelper" can see it. if lev != JS_FINE: for issue in issues: err.append("[Non-crash bug] " + issue) activated = False # Turn on when trying to report *reliable* testcases that do not have a coredump # On Linux, fall back to run testcase via gdb using --args if core file data is unavailable # Note that this second round of running uses a different fuzzSeed as the initial if default jsfunfuzz is run # We should separate this out, i.e. running jsfunfuzz within a debugger, only if core dumps cannot be generated if activated and platform.system() == "Linux" and which( "gdb") and not auxCrashData and not in_compare_jit: print( "Note: No core file found on Linux - falling back to run via gdb" ) extracted_gdb_cmds = ["-ex", "run"] with io.open(str( Path(__file__).parent.parent / "util" / "gdb_cmds.txt"), "r", encoding="utf-8", errors="replace") as f: for line in f: if line.rstrip() and not line.startswith( "#") and not line.startswith("echo"): extracted_gdb_cmds.append("-ex") extracted_gdb_cmds.append("%s" % line.rstrip()) no_main_log_gdb_log = subprocess.run( (["gdb", "-n", "-batch"] + extracted_gdb_cmds + ["--args"] + [str(x) for x in runthis]), check=True, stderr=subprocess.PIPE, stdout=subprocess.PIPE) auxCrashData = no_main_log_gdb_log.stdout # Finally, make a CrashInfo object and parse stack traces for asan/crash/assertion bugs crashInfo = CrashInfo.CrashInfo.fromRawCrashData( out, err, pc, auxCrashData=auxCrashData) create_collector.printCrashInfo(crashInfo) # We only care about crashes and assertion failures on shells with no symbols # Note that looking out for the Assertion failure message is highly SpiderMonkey-specific if not isinstance(crashInfo, CrashInfo.NoCrashInfo) or \ "Assertion failure: " in str(crashInfo.rawStderr) or \ "Segmentation fault" in str(crashInfo.rawStderr) or \ "Bus error" in str(crashInfo.rawStderr): lev = max(lev, JS_NEW_ASSERT_OR_CRASH) try: match = options.collector.search(crashInfo) if match[0] is not None: create_collector.printMatchingSignature(match) lev = JS_FINE except UnicodeDecodeError: # Sometimes FM throws due to unicode issues print( "Note: FuzzManager is throwing a UnicodeDecodeError, signature matching skipped" ) match = False print("%s | %s" % (logPrefix, summaryString(issues, lev, runinfo.elapsedtime))) if lev != JS_FINE: summary_log = (logPrefix.parent / (logPrefix.stem + "-summary")).with_suffix(".txt") with io.open(str(summary_log), "w", encoding="utf-8", errors="replace") as f: f.writelines([ "Number: " + str(logPrefix) + "\n", "Command: " + " ".join(quote(str(x)) for x in runthis) + "\n" ] + ["Status: " + i + "\n" for i in issues]) self.lev = lev self.out = out self.err = err self.issues = issues self.crashInfo = crashInfo # pylint: disable=invalid-name self.match = match self.runinfo = runinfo self.return_code = runinfo.return_code