# coding=utf-8 # # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at https://mozilla.org/MPL/2.0/. """Ensures a build is available, then forks a bunch of fuzz-reduce processes. """ import io import multiprocessing from optparse import OptionParser # pylint: disable=deprecated-module from pathlib import Path import platform import shutil import subprocess import sys import tempfile import time from EC2Reporter.EC2Reporter import EC2Reporter from .js import build_options from .js import compile_shell from .js import loop from .util import create_collector from .util import fork_join from .util import hg_helpers from .util import sm_compile_helpers from .util.lock_dir import LockDir JS_SHELL_DEFAULT_TIMEOUT = 24 # see comments in loop for tradeoffs class BuildInfo: # pylint: disable=missing-param-doc,missing-type-doc,too-few-public-methods """Store information related to the build, such as its directory, source and type.""" def __init__(self, bDir, bType, bSrc, bRev, manyTimedRunArgs): # pylint: disable=too-many-arguments self.buildDir = bDir # pylint: disable=invalid-name self.buildType = bType # pylint: disable=invalid-name self.buildSrc = bSrc # pylint: disable=invalid-name self.buildRev = bRev # pylint: disable=invalid-name self.mtrArgs = manyTimedRunArgs # pylint: disable=invalid-name def parseOpts(): # pylint: disable=invalid-name,missing-docstring,missing-return-doc,missing-return-type-doc parser = OptionParser() parser.set_defaults( repoName="mozilla-central", targetTime=15 * 60, # 15 minutes existingBuildDir=None, timeout=0, build_options=None, useTreeherderBuilds=False, ) parser.add_option("--build", dest="existingBuildDir", help="Use an existing build directory.") parser.add_option("--repotype", dest="repoName", help='Sets the repository to be fuzzed. Defaults to "%default".') parser.add_option("--target-time", dest="targetTime", type="int", help="Nominal amount of time to run, in seconds") parser.add_option("-T", "--use-treeherder-builds", dest="useTreeherderBuilds", action="store_true", help="Download builds from treeherder instead of compiling our own.") # Specify how the shell will be built. parser.add_option("-b", "--build-options", dest="build_options", help='Specify build options, e.g. -b "-c opt --arch=32" for js ' "(python -m funfuzz.js.build_options --help)") parser.add_option("--timeout", type="int", dest="timeout", help="Sets the timeout for loop. " "Defaults to taking into account the speed of the computer and debugger (if any).") options, args = parser.parse_args() if args: print("Warning: bot does not use positional arguments") if not options.useTreeherderBuilds and not build_options.DEFAULT_TREES_LOCATION.is_dir(): # We don't have trees, so we must use treeherder builds. options.useTreeherderBuilds = True print() print(f"Trees were absent from default location: {build_options.DEFAULT_TREES_LOCATION}") print("Using treeherder builds instead...") print() sys.exit("Fuzzing downloaded builds is disabled for now, until tooltool is removed. Exiting...") if options.build_options is None: options.build_options = "" if options.useTreeherderBuilds and options.build_options != "": raise Exception("Do not use treeherder builds if one specifies build parameters") return options def main(): # pylint: disable=missing-docstring print_machine_info() options = parseOpts() collector = create_collector.make_collector() try: collector.refresh() except RuntimeError: print() print("Unable to find required entries in FuzzManager. Duplicate detection via sigcache will not work...") options.tempDir = tempfile.mkdtemp("fuzzbot") print(options.tempDir) build_info = ensureBuild(options) assert build_info.buildDir.is_dir() number_of_processes = multiprocessing.cpu_count() if "-asan" in str(build_info.buildDir): # This should really be based on the amount of RAM available, but I don't know how to compute that in Python. # I could guess 1 GB RAM per core, but that wanders into sketchyville. number_of_processes = max(number_of_processes // 2, 1) try: EC2Reporter().report(f"About to start fuzzing {number_of_processes} \n" f" {options.build_options.build_options_str} \n" f" with target time {options.targetTime} \n" f" and jsfunfuzz timeout of {options.timeout} ...") except RuntimeError: # Ignore errors if the server is temporarily unavailable print("Failed to contact server") fork_join.forkJoin(options.tempDir, number_of_processes, loopFuzzingAndReduction, options, build_info, collector) try: EC2Reporter().report("Fuzzing has finished...") except RuntimeError: # Ignore errors if the server is temporarily unavailable print("Failed to contact server") shutil.rmtree(options.tempDir) def print_machine_info(): """Log information about the machine.""" print(f'Platform details: {" ".join(platform.uname())}') hg_version = subprocess.run(["hg", "-q", "version"], check=True, stdout=subprocess.PIPE).stdout.decode("utf-8", errors="replace").rstrip() print(f"hg info: {hg_version}") if shutil.which("gdb"): gdb_version = subprocess.run(["gdb", "--version"], check=True, stdout=subprocess.PIPE).stdout.decode("utf-8", errors="replace").split("\n")[0] print(f"gdb info: {gdb_version}") if shutil.which("git"): git_version = subprocess.run(["git", "version"], check=True, stdout=subprocess.PIPE).stdout.decode("utf-8", errors="replace").rstrip() print(f"git info: {git_version}") print(f"Python version: {sys.version.split()[0]}") print(f"Number of cores visible to OS: {multiprocessing.cpu_count()}") rootdir_free_space = shutil.disk_usage("/").free / (1024 ** 3) print(f"Free space (GB): {rootdir_free_space:.2f}") hgrc_path = Path("~/.hg/hgrc").expanduser() if hgrc_path.is_file(): print("The hgrc of this repository is:") with io.open(str(hgrc_path), "r", encoding="utf-8", errors="replace") as f: hgrc_contents = f.readlines() for line in hgrc_contents: print(line.rstrip()) try: # resource library is only applicable to Linux or Mac platforms. import resource # pylint: disable=import-error # pylint: disable=no-member print(f"Corefile size (soft limit, hard limit) is: {resource.getrlimit(resource.RLIMIT_CORE)!r}") except ImportError: print("Not checking corefile size as resource module is unavailable") def ensureBuild(options): # pylint: disable=invalid-name,missing-docstring,missing-return-doc,missing-return-type-doc if options.existingBuildDir: # Pre-downloaded treeherder builds bDir = options.existingBuildDir # pylint: disable=invalid-name bType = "local-build" # pylint: disable=invalid-name bSrc = bDir # pylint: disable=invalid-name bRev = "" # pylint: disable=invalid-name manyTimedRunArgs = [] # pylint: disable=invalid-name elif not options.useTreeherderBuilds: options.build_options = build_options.parse_shell_opts(options.build_options) options.timeout = options.timeout or (300 if options.build_options.runWithVg else JS_SHELL_DEFAULT_TIMEOUT) with LockDir(sm_compile_helpers.get_lock_dir_path(Path.home(), options.build_options.repo_dir)): bRev = hg_helpers.get_repo_hash_and_id(options.build_options.repo_dir)[0] # pylint: disable=invalid-name cshell = compile_shell.CompiledShell(options.build_options, bRev) updateLatestTxt = (options.build_options.repo_dir == "mozilla-central") # pylint: disable=invalid-name compile_shell.obtainShell(cshell, updateLatestTxt=updateLatestTxt) bDir = cshell.get_shell_cache_dir() # pylint: disable=invalid-name # Strip out first 3 chars or else the dir name in fuzzing jobs becomes: # js-js-dbg-opt-64-dm-linux bType = build_options.computeShellType(options.build_options)[3:] # pylint: disable=invalid-name bSrc = ( # pylint: disable=invalid-name f"Create another shell in shell-cache like this one:\n" f"python3 -u -m funfuzz.js.compile_shell " f'-b "{options.build_options.build_options_str} ' f'-R {options.build_options.repo_dir}" ' f"-r {bRev}\n\n" f"==============================================\n" f"| Fuzzing {cshell.get_repo_name()} js shell builds\n" f"| DATE: {time.asctime()}\n" f"==============================================\n\n" ) manyTimedRunArgs = mtrArgsCreation(options, cshell) # pylint: disable=invalid-name print(f"buildDir is: {bDir}") print(f"buildSrc is: {bSrc}") else: print("TBD: We need to switch to the fuzzfetch repository.") sys.exit(0) return BuildInfo(bDir, bType, bSrc, bRev, manyTimedRunArgs) def loopFuzzingAndReduction(options, buildInfo, collector, i): # pylint: disable=invalid-name,missing-docstring tempDir = Path(tempfile.mkdtemp(f"loop{i}")) # pylint: disable=invalid-name loop.many_timed_runs(options.targetTime, tempDir, buildInfo.mtrArgs, collector, False) def mtrArgsCreation(options, cshell): # pylint: disable=invalid-name,missing-param-doc,missing-return-doc # pylint: disable=missing-return-type-doc,missing-type-doc """Create many_timed_run arguments for compiled builds.""" manyTimedRunArgs = [] # pylint: disable=invalid-name manyTimedRunArgs.append(f"--repo={options.build_options.repo_dir}") manyTimedRunArgs.append(f"--build={options.build_options.build_options_str}") if options.build_options.runWithVg: manyTimedRunArgs.append("--valgrind") if options.build_options.enableMoreDeterministic: # Treeherder shells not using compare_jit: # They are not built with --enable-more-deterministic - bug 751700 manyTimedRunArgs.append("--compare-jit") manyTimedRunArgs.append("--random-flags") # Ordering of elements in manyTimedRunArgs is important. manyTimedRunArgs.append(str(options.timeout)) manyTimedRunArgs.append(cshell.get_repo_name()) # known bugs' directory manyTimedRunArgs.append(cshell.get_shell_cache_js_bin_path()) return manyTimedRunArgs if __name__ == "__main__": main()