blob: 0bdf2095ac78b7045baa2af103b2f2edd703c760 [file]
#!/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())