Adrian Taylor | 99741893 | 2024-12-20 15:21:32 | [diff] [blame] | 1 | #!/usr/bin/env python3 |
| 2 | # Copyright 2024 The Chromium Authors |
| 3 | # Use of this source code is governed by a BSD-style license that can be |
| 4 | # found in the LICENSE file. |
| 5 | """Provides a local HTML report of the ClusterFuzz explorations |
| 6 | by a given fuzzer. |
| 7 | |
| 8 | * Example usage: view_fuzz_coverage.py --fuzzer my_fuzzer_binary |
| 9 | """ |
| 10 | |
| 11 | import argparse |
| 12 | import os |
| 13 | import subprocess |
| 14 | import sys |
| 15 | import tempfile |
| 16 | import pathlib |
| 17 | |
| 18 | script_dir = os.path.dirname(os.path.realpath(__file__)) |
| 19 | chromium_src_dir = os.path.dirname(os.path.dirname(script_dir)) |
| 20 | |
| 21 | # These may evolve over time, so if this script doesn't work, you may |
| 22 | # need to adjust these. In an ideal world we'd look these up from LUCI |
| 23 | # infrastructure but we're intentionally making a local script somewhat |
| 24 | # equivalentt to LUCI infrastructure, so for now let's not rely on that. |
| 25 | gn_args = """ |
| 26 | dcheck_always_on = false |
| 27 | enable_mojom_fuzzer = true |
| 28 | ffmpeg_branding = "ChromeOS" |
| 29 | is_component_build = false |
| 30 | is_debug = false |
| 31 | pdf_enable_xfa = true |
| 32 | proprietary_codecs = true |
| 33 | use_clang_coverage = true |
| 34 | use_libfuzzer = true |
| 35 | use_remoteexec = true |
| 36 | symbol_level = 2 |
| 37 | """ |
| 38 | |
| 39 | |
| 40 | def _ParseCommandArguments(): |
| 41 | """Adds and parses relevant arguments for tool comands. |
| 42 | |
| 43 | Returns: |
| 44 | A dictionary representing the arguments. |
| 45 | """ |
| 46 | arg_parser = argparse.ArgumentParser() |
| 47 | arg_parser.usage = __doc__ |
| 48 | |
| 49 | arg_parser.add_argument('--fuzzer', |
| 50 | required=True, |
| 51 | type=str, |
| 52 | help='Fuzzer binary name.') |
| 53 | arg_parser.add_argument('--build-dir', |
| 54 | default=os.path.join(chromium_src_dir, 'out', |
| 55 | 'coverage'), |
| 56 | help='Where to build fuzzers.') |
| 57 | arg_parser.add_argument('--html-dir', |
| 58 | default=os.path.join(chromium_src_dir, 'out', |
| 59 | 'coverage-html'), |
| 60 | help='Where to put HTML report.') |
| 61 | arg_parser.add_argument( |
| 62 | '--retain-build-dir', |
| 63 | action='store_true', |
| 64 | help= |
| 65 | 'Avoid cleaning the build dir (may result in multiple fuzzers being analyzed).' |
| 66 | ) |
| 67 | args = arg_parser.parse_args() |
| 68 | return args |
| 69 | |
| 70 | |
| 71 | def step(name): |
| 72 | """Print a banner for the upcoming task..""" |
| 73 | print("==== " + name + " ====:") |
| 74 | |
| 75 | |
| 76 | def check_call(args, *, cwd=None, shell=False): |
| 77 | """Equivalent to subprocess.check_call but logs command.""" |
| 78 | print(" ".join(args)) |
| 79 | subprocess.check_call(args, cwd=cwd, shell=shell) |
| 80 | |
| 81 | |
| 82 | def Main(): |
| 83 | args = _ParseCommandArguments() |
| 84 | |
| 85 | os.makedirs(args.build_dir, exist_ok=True) |
| 86 | os.makedirs(args.html_dir, exist_ok=True) |
| 87 | |
| 88 | step("Writing gn args") |
| 89 | gn_args_file = os.path.join(args.build_dir, "args.gn") |
| 90 | with open(gn_args_file, "w") as f: |
| 91 | f.write(gn_args) |
| 92 | |
| 93 | if not args.retain_build_dir: |
| 94 | step("gn clean") |
| 95 | check_call(["gn", "clean", args.build_dir], cwd=chromium_src_dir) |
| 96 | step("gn gen") |
| 97 | check_call(["gn", "gen", args.build_dir], cwd=chromium_src_dir) |
| 98 | step("autoninja") |
| 99 | check_call(["autoninja", "-C", args.build_dir, args.fuzzer]) |
| 100 | corpora_dir = tempfile.TemporaryDirectory() |
| 101 | step("Download corpora") |
| 102 | check_call([ |
| 103 | sys.executable, |
| 104 | os.path.join(script_dir, "download_fuzz_corpora.py"), "--download-dir", |
| 105 | corpora_dir.name, "--build-dir", args.build_dir |
| 106 | ]) |
| 107 | individual_profdata_dir = tempfile.TemporaryDirectory() |
| 108 | step( |
| 109 | "Running fuzzers (can take a while - NB you might need a valid DISPLAY set for some fuzzers)" |
| 110 | ) |
| 111 | check_call([ |
| 112 | sys.executable, |
| 113 | os.path.join(script_dir, "run_all_fuzzers.py"), "--fuzzer-binaries-dir", |
| 114 | args.build_dir, "--fuzzer-corpora-dir", corpora_dir.name, |
| 115 | "--profdata-outdir", individual_profdata_dir.name |
| 116 | ]) |
| 117 | step("Merging profdata") |
| 118 | merged_profdata_dir = tempfile.TemporaryDirectory() |
| 119 | merged_profdata_file = os.path.join(merged_profdata_dir.name, "out.profdata") |
| 120 | llvm_dir = os.path.join(chromium_src_dir, "third_party", "llvm-build", |
| 121 | "Release+Asserts", "bin") |
| 122 | check_call([ |
| 123 | sys.executable, |
| 124 | os.path.join(script_dir, "merge_all_profdata.py"), "--profdata-dir", |
| 125 | individual_profdata_dir.name, "--outfile", merged_profdata_file, |
| 126 | "--llvm-profdata", |
| 127 | os.path.join(llvm_dir, "llvm-profdata") |
| 128 | ]) |
| 129 | step("Generating HTML") |
| 130 | check_call([ |
| 131 | os.path.join(llvm_dir, "llvm-cov"), "show", args.fuzzer, "-format=html", |
| 132 | "-instr-profile", merged_profdata_file, "-output-dir", args.html_dir |
| 133 | ], |
| 134 | cwd=args.build_dir) |
| 135 | uri = pathlib.Path(os.path.join(args.html_dir, "index.html")).as_uri() |
| 136 | print("Report URI " + uri) |
| 137 | step("Opening HTML in Chrome") |
| 138 | check_call(["google-chrome-stable", uri], shell=True) |
| 139 | |
| 140 | |
| 141 | if __name__ == '__main__': |
| 142 | sys.exit(Main()) |