diff --git a/docs/code_coverage.md b/docs/code_coverage.md index 285643930..b37f03a78 100644 --- a/docs/code_coverage.md +++ b/docs/code_coverage.md @@ -40,6 +40,8 @@ and try again. Once `gsutil` works, you can run the report generation. ## Generating code coverage reports +### Full project report + To generate code coverage report using the corpus aggregated on OSS-Fuzz, run: ```bash @@ -54,6 +56,22 @@ each fuzz target, then run: python infra/helper.py profile --no-corpus-download $project_name ``` +### Single fuzz target + +You can generate a code coverage report for a particular fuzz target with +`--fuzz-target` argument: + +```bash +python infra/helper.py profile --fuzz-target= $project_name +``` + +In this mode, you can specify an arbitrary corpus location for the fuzz target +via `--corpus-dir` to be used instead of the corpus downloaded from OSS-Fuzz: + +```bash +python infra/helper.py profile --fuzz-target= --corpus-dir= $project_name +``` + [Clang Source-based Code Coverage]: https://clang.llvm.org/docs/SourceBasedCodeCoverage.html [gsutil tool]: https://cloud.google.com/storage/docs/gsutil_install diff --git a/infra/base-images/base-runner/coverage b/infra/base-images/base-runner/coverage index 57d8bc4cc..e70efeea9 100755 --- a/infra/base-images/base-runner/coverage +++ b/infra/base-images/base-runner/coverage @@ -16,6 +16,12 @@ ################################################################################ cd $OUT +if (( $# > 0 )); then + FUZZ_TARGETS="$@" +else + FUZZ_TARGETS="$(find . -maxdepth 1 -type f -executable)" +fi + LOGS_DIR="$OUT/logs" mkdir -p $LOGS_DIR @@ -39,7 +45,7 @@ function run_fuzz_target { } # Run each fuzz target, generate raw coverage dumps. -for fuzz_target in $(find . -maxdepth 1 -type f -executable); do +for fuzz_target in $FUZZ_TARGETS; do echo "Running $fuzz_target" run_fuzz_target $fuzz_target & objects="$objects -object=$fuzz_target" diff --git a/infra/helper.py b/infra/helper.py index 1b788db8f..021bd6080 100755 --- a/infra/helper.py +++ b/infra/helper.py @@ -128,6 +128,10 @@ def main(): 'use corpus located in build/corpus///') profile_parser.add_argument('--port', default='8008', help='specify port for ' 'a local HTTP server rendering coverage report') + profile_parser.add_argument('--fuzz-target', help='specify name of a fuzz ' + 'target to be run for generating coverage report') + profile_parser.add_argument('--corpus-dir', help='specify location of corpus ' + 'to be used (requires --fuzz-target argument)') reproduce_parser = subparsers.add_parser( 'reproduce', help='Reproduce a crash.') @@ -530,7 +534,7 @@ def _get_latest_corpus(project_name, fuzz_target, base_corpus_dir): subprocess.check_call(command) -def download_corpus(project_name): +def download_corpus(args): """Download most recent corpus from GCS for the given project.""" try: with open(os.devnull, 'w') as stdout: @@ -541,32 +545,43 @@ def download_corpus(project_name): file=sys.stderr) return False - fuzz_targets = _get_fuzz_targets(project_name) - corpus_dir = _get_corpus_dir(project_name) + if args.fuzz_target: + fuzz_targets = [args.fuzz_target] + else: + fuzz_targets = _get_fuzz_targets(args.project_name) + + corpus_dir = _get_corpus_dir(args.project_name) if not os.path.exists(corpus_dir): os.makedirs(corpus_dir) def _download_for_single_target(fuzz_target): try: - _get_latest_corpus(project_name, fuzz_target, corpus_dir) + _get_latest_corpus(args.project_name, fuzz_target, corpus_dir) return True except Exception as e: print('ERROR: corpus download for %s failed: %s' % (fuzz_target, str(e)), file=sys.stderr) return False - print('Downloading corpus for %s project' % project_name) + print('Downloading corpus for %s project' % args.project_name) thread_pool = ThreadPool(multiprocessing.cpu_count()) return all(thread_pool.map(_download_for_single_target, fuzz_targets)) def profile(args): """Generate code coverage using clang source based code coverage.""" + if args.corpus_dir and not args.fuzz_target: + print('ERROR: --corpus-dir requires specifying a particular fuzz target ' + 'using --fuzz-target', + file=sys.stderr) + return 1 + if not _check_project_exists(args.project_name): return 1 - if not args.no_corpus_download: - if not download_corpus(args.project_name): - return 1 + + if not args.no_corpus_download and not args.corpus_dir: + if not download_corpus(args): + return 1 env = [ 'FUZZING_ENGINE=libfuzzer', @@ -575,14 +590,27 @@ def profile(args): 'HTTP_PORT=%s' % args.port, ] - run_args = _env_to_docker_args(env) + [ + run_args = _env_to_docker_args(env) + + if args.corpus_dir: + if not os.path.exists(args.corpus_dir): + print('ERROR: the path provided in --corpus-dir argument does not exist', + file=sys.stderr) + return 1 + corpus_dir = os.path.realpath(args.corpus_dir) + run_args.extend(['-v', '%s:/corpus/%s' % (corpus_dir, args.fuzz_target)]) + else: + run_args.extend(['-v', '%s:/corpus' % _get_corpus_dir(args.project_name)]) + + run_args.extend([ '-v', '%s:/out' % _get_output_dir(args.project_name), - '-v', '%s:/corpus' % _get_corpus_dir(args.project_name), '-p', '%s:%s' % (args.port, args.port), '-t', 'gcr.io/oss-fuzz-base/base-runner', - ] + ]) run_args.append('coverage') + if args.fuzz_target: + run_args.append(args.fuzz_target) exit_code = docker_run(run_args) if exit_code == 0: