|
| 1 | +#!/usr/bin/env python3 |
| 2 | +# Copyright (c) 2019 The Bitcoin Core developers |
| 3 | +# Distributed under the MIT software license, see the accompanying |
| 4 | +# file COPYING or http://www.opensource.org/licenses/mit-license.php. |
| 5 | +"""Run fuzz test targets. |
| 6 | +""" |
| 7 | + |
| 8 | +import argparse |
| 9 | +import configparser |
| 10 | +import os |
| 11 | +import sys |
| 12 | +import subprocess |
| 13 | +import logging |
| 14 | + |
| 15 | + |
| 16 | +def main(): |
| 17 | + parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter) |
| 18 | + parser.add_argument( |
| 19 | + "-l", |
| 20 | + "--loglevel", |
| 21 | + dest="loglevel", |
| 22 | + default="INFO", |
| 23 | + help="log events at this level and higher to the console. Can be set to DEBUG, INFO, WARNING, ERROR or CRITICAL. Passing --loglevel DEBUG will output all logs to console.", |
| 24 | + ) |
| 25 | + parser.add_argument( |
| 26 | + '--export_coverage', |
| 27 | + action='store_true', |
| 28 | + help='If true, export coverage information to files in the seed corpus', |
| 29 | + ) |
| 30 | + parser.add_argument( |
| 31 | + 'seed_dir', |
| 32 | + help='The seed corpus to run on (must contain subfolders for each fuzz target).', |
| 33 | + ) |
| 34 | + parser.add_argument( |
| 35 | + 'target', |
| 36 | + nargs='*', |
| 37 | + help='The target(s) to run. Default is to run all targets.', |
| 38 | + ) |
| 39 | + |
| 40 | + args = parser.parse_args() |
| 41 | + |
| 42 | + # Set up logging |
| 43 | + logging.basicConfig( |
| 44 | + format='%(message)s', |
| 45 | + level=int(args.loglevel) if args.loglevel.isdigit() else args.loglevel.upper(), |
| 46 | + ) |
| 47 | + |
| 48 | + # Read config generated by configure. |
| 49 | + config = configparser.ConfigParser() |
| 50 | + configfile = os.path.abspath(os.path.dirname(__file__)) + "/../config.ini" |
| 51 | + config.read_file(open(configfile, encoding="utf8")) |
| 52 | + |
| 53 | + if not config["components"].getboolean("ENABLE_FUZZ"): |
| 54 | + logging.error("Must have fuzz targets built") |
| 55 | + sys.exit(1) |
| 56 | + |
| 57 | + # Build list of tests |
| 58 | + test_list_all = parse_test_list(makefile=os.path.join(config["environment"]["SRCDIR"], 'src', 'Makefile.test.include')) |
| 59 | + |
| 60 | + if not test_list_all: |
| 61 | + logging.error("No fuzz targets found") |
| 62 | + sys.exit(1) |
| 63 | + |
| 64 | + logging.info("Fuzz targets found: {}".format(test_list_all)) |
| 65 | + |
| 66 | + args.target = args.target or test_list_all # By default run all |
| 67 | + test_list_error = list(set(args.target).difference(set(test_list_all))) |
| 68 | + if test_list_error: |
| 69 | + logging.error("Unknown fuzz targets selected: {}".format(test_list_error)) |
| 70 | + test_list_selection = list(set(test_list_all).intersection(set(args.target))) |
| 71 | + if not test_list_selection: |
| 72 | + logging.error("No fuzz targets selected") |
| 73 | + logging.info("Fuzz targets selected: {}".format(test_list_selection)) |
| 74 | + |
| 75 | + help_output = subprocess.run( |
| 76 | + args=[ |
| 77 | + os.path.join(config["environment"]["BUILDDIR"], 'src', 'test', 'fuzz', test_list_selection[0]), |
| 78 | + '-help=1', |
| 79 | + ], |
| 80 | + check=True, |
| 81 | + stderr=subprocess.PIPE, |
| 82 | + universal_newlines=True, |
| 83 | + ).stderr |
| 84 | + if "libFuzzer" not in help_output: |
| 85 | + logging.error("Must be built with libFuzzer") |
| 86 | + sys.exit(1) |
| 87 | + |
| 88 | + run_once( |
| 89 | + corpus=args.seed_dir, |
| 90 | + test_list=test_list_selection, |
| 91 | + build_dir=config["environment"]["BUILDDIR"], |
| 92 | + export_coverage=args.export_coverage, |
| 93 | + ) |
| 94 | + |
| 95 | + |
| 96 | +def run_once(*, corpus, test_list, build_dir, export_coverage): |
| 97 | + for t in test_list: |
| 98 | + args = [ |
| 99 | + os.path.join(build_dir, 'src', 'test', 'fuzz', t), |
| 100 | + '-runs=1', |
| 101 | + os.path.join(corpus, t), |
| 102 | + ] |
| 103 | + logging.debug('Run {} with args {}'.format(t, args)) |
| 104 | + output = subprocess.run(args, check=True, stderr=subprocess.PIPE, universal_newlines=True).stderr |
| 105 | + logging.debug('Output: {}'.format(output)) |
| 106 | + if not export_coverage: |
| 107 | + continue |
| 108 | + for l in output.splitlines(): |
| 109 | + if 'INITED' in l: |
| 110 | + with open(os.path.join(corpus, t + '_coverage'), 'w', encoding='utf-8') as cov_file: |
| 111 | + cov_file.write(l) |
| 112 | + break |
| 113 | + |
| 114 | + |
| 115 | +def parse_test_list(makefile): |
| 116 | + with open(makefile, encoding='utf-8') as makefile_test: |
| 117 | + test_list_all = [] |
| 118 | + read_targets = False |
| 119 | + for line in makefile_test.readlines(): |
| 120 | + line = line.strip().replace('test/fuzz/', '').replace(' \\', '') |
| 121 | + if read_targets: |
| 122 | + if not line: |
| 123 | + break |
| 124 | + test_list_all.append(line) |
| 125 | + continue |
| 126 | + |
| 127 | + if line == 'FUZZ_TARGETS =': |
| 128 | + read_targets = True |
| 129 | + return test_list_all |
| 130 | + |
| 131 | + |
| 132 | +if __name__ == '__main__': |
| 133 | + main() |
0 commit comments