| #!/usr/bin/env vpython3 |
| # Copyright 2024 The Chromium Authors |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| """Provides a local HTML report of the ClusterFuzz explorations |
| by a given fuzzer. |
| |
| * Example usage: vpython3 view_fuzz_coverage.py --target my_fuzzer_binary |
| --fuzzer-type libfuzzer |
| """ |
| |
| import argparse |
| import os |
| import subprocess |
| import sys |
| import tempfile |
| import pathlib |
| |
| script_dir = os.path.dirname(os.path.realpath(__file__)) |
| chromium_src_dir = os.path.dirname(os.path.dirname(script_dir)) |
| |
| XVFB_PATH = os.path.join(chromium_src_dir, 'testing/xvfb.py') |
| |
| # These may evolve over time, so if this script doesn't work, you may |
| # need to adjust these. In an ideal world we'd look these up from LUCI |
| # infrastructure but we're intentionally making a local script somewhat |
| # equivalent to LUCI infrastructure, so for now let's not rely on that. |
| gn_args_libfuzzer = """ |
| dcheck_always_on = false |
| enable_mojom_fuzzer = true |
| ffmpeg_branding = "ChromeOS" |
| is_component_build = false |
| is_debug = false |
| pdf_enable_xfa = true |
| proprietary_codecs = true |
| use_clang_coverage = true |
| use_clang_modules = false |
| use_libfuzzer = true |
| use_remoteexec = true |
| symbol_level = 2 |
| """ |
| |
| gn_args_blackbox = """ |
| dcheck_always_on = false |
| is_asan = true |
| is_component_build = false |
| is_debug = false |
| use_clang_coverage = true |
| use_clang_modules = false |
| use_remoteexec = true |
| symbol_level = 2 |
| """ |
| |
| |
| def _ParseCommandArguments(): |
| """Adds and parses relevant arguments for tool comands. |
| |
| Returns: |
| A dictionary representing the arguments. |
| """ |
| arg_parser = argparse.ArgumentParser() |
| arg_parser.usage = __doc__ |
| |
| arg_parser.add_argument( |
| '--fuzzer-type', |
| choices=['libfuzzer', 'centipede', 'fuzzilli', 'blackbox'], |
| default='libfuzzer', |
| help='The type of fuzzer to analyze.', |
| ) |
| arg_parser.add_argument( |
| '--target', |
| type=str, |
| required=True, |
| help='The fuzzer binary name. For blackbox fuzzers, ' |
| 'the target binary to use, e.g. Chrome or v8', |
| ) |
| arg_parser.add_argument( |
| '--build-dir', |
| default=os.path.join(chromium_src_dir, 'out', 'coverage'), |
| help='Where to build fuzzers.', |
| ) |
| arg_parser.add_argument( |
| '--html-dir', |
| default=os.path.join(chromium_src_dir, 'out', 'coverage-html'), |
| help='Where to put HTML report.', |
| ) |
| arg_parser.add_argument( |
| '--corpora-dir', |
| help='The directory containing the fuzzing corpora. Required when the ' |
| 'fuzzer type is blackbox, where it is the directory of html or js files ' |
| 'to be run with the target.', |
| ) |
| arg_parser.add_argument( |
| '--retain-build-dir', |
| action='store_true', |
| help='Avoid cleaning the build dir (may result in multiple fuzzers being ' |
| 'analyzed).', |
| ) |
| arg_parser.add_argument( |
| '--testcase-timeout', |
| type=int, |
| default=60, |
| help='Timeout in seconds for each testcase. Defaults to 60 seconds. For ' |
| 'blackbox fuzzers, this corresponds to the TEST_TIMEOUT env variable for ' |
| 'the bot running the fuzzer. Many blackbox fuzzers require a timeout ' |
| 'because test cases can\'t signal when they are finished.', |
| ) |
| args = arg_parser.parse_args() |
| return args |
| |
| |
| def step(name): |
| """Print a banner for the upcoming task..""" |
| print("==== " + name + " ====:") |
| |
| |
| def check_call(args, *, cwd=None, shell=False): |
| """Equivalent to subprocess.check_call but logs command.""" |
| print(" ".join(args)) |
| subprocess.check_call(args, cwd=cwd, shell=shell) |
| |
| |
| def Main(): |
| args = _ParseCommandArguments() |
| |
| os.makedirs(args.build_dir, exist_ok=True) |
| os.makedirs(args.html_dir, exist_ok=True) |
| |
| step("Writing gn args") |
| gn_args_file = os.path.join(args.build_dir, "args.gn") |
| with open(gn_args_file, "w") as f: |
| if args.fuzzer_type == 'blackbox': |
| f.write(gn_args_blackbox) |
| else: |
| f.write(gn_args_libfuzzer) |
| |
| if not args.retain_build_dir: |
| step("gn clean") |
| check_call(["gn", "clean", args.build_dir], cwd=chromium_src_dir) |
| step("gn gen") |
| check_call(["gn", "gen", args.build_dir], cwd=chromium_src_dir) |
| |
| step("autoninja") |
| check_call(["autoninja", "-C", args.build_dir, args.target]) |
| |
| is_blackbox_fuzzer = args.fuzzer_type == 'blackbox' |
| |
| if is_blackbox_fuzzer: |
| if not args.corpora_dir: |
| print("--corpora-dir is required for blackbox fuzzer coverage.") |
| return 1 |
| |
| corpora_dir_path = args.corpora_dir |
| else: |
| temp_corpora_dir = tempfile.TemporaryDirectory() |
| corpora_dir_path = temp_corpora_dir.name |
| step("Download corpora") |
| check_call( |
| [ |
| sys.executable, |
| os.path.join(script_dir, "download_fuzz_corpora.py"), |
| "--download-dir", |
| corpora_dir_path, |
| "--build-dir", |
| args.build_dir, |
| "--corpora-type", |
| args.fuzzer_type, |
| ] |
| ) |
| |
| individual_profdata_dir = tempfile.TemporaryDirectory() |
| step( |
| 'Running fuzzers (can take a while - NB you might need a valid DISPLAY ' |
| 'set for some fuzzers)' |
| ) |
| |
| run_all_fuzzers_cmd = [ |
| sys.executable, |
| XVFB_PATH, |
| os.path.join(script_dir, "run_all_fuzzers.py"), |
| "--fuzzer-binaries-dir", |
| args.build_dir, |
| "--fuzzer-corpora-dir", |
| corpora_dir_path, |
| "--profdata-outdir", |
| individual_profdata_dir.name, |
| "--fuzzer", |
| args.fuzzer_type, |
| "--testcase-timeout", |
| str(args.testcase_timeout), |
| ] |
| if is_blackbox_fuzzer: |
| run_all_fuzzers_cmd += ["--target", args.target] |
| |
| check_call(run_all_fuzzers_cmd) |
| |
| step("Merging profdata") |
| merged_profdata_dir = tempfile.TemporaryDirectory() |
| merged_profdata_file = os.path.join(merged_profdata_dir.name, "out.profdata") |
| llvm_dir = os.path.join( |
| chromium_src_dir, "third_party", "llvm-build", "Release+Asserts", "bin" |
| ) |
| check_call( |
| [ |
| sys.executable, |
| os.path.join(script_dir, "merge_all_profdata.py"), |
| "--profdata-dir", |
| individual_profdata_dir.name, |
| "--outfile", |
| merged_profdata_file, |
| "--llvm-profdata", |
| os.path.join(llvm_dir, "llvm-profdata"), |
| ] |
| ) |
| |
| step("Generating HTML") |
| target_path = os.path.join(args.build_dir, args.target) |
| check_call( |
| [ |
| os.path.join(llvm_dir, "llvm-cov"), |
| "show", |
| target_path, |
| "--format=html", |
| "--instr-profile", |
| merged_profdata_file, |
| "--output-dir", |
| args.html_dir, |
| "--compilation-dir", |
| args.build_dir, |
| ] |
| ) |
| |
| uri = pathlib.Path( |
| os.path.abspath(os.path.join(args.html_dir, "index.html")) |
| ).as_uri() |
| print("Report URI " + uri) |
| step("Opening HTML in Chrome") |
| check_call(["google-chrome-stable", uri]) |
| |
| |
| if __name__ == '__main__': |
| sys.exit(Main()) |