Changeset View
Changeset View
Standalone View
Standalone View
test/fuzz/test_runner.py
#!/usr/bin/env python3 | #!/usr/bin/env python3 | ||||
# Copyright (c) 2019 The Bitcoin Core developers | # Copyright (c) 2019 The Bitcoin Core developers | ||||
# Distributed under the MIT software license, see the accompanying | # Distributed under the MIT software license, see the accompanying | ||||
# file COPYING or http://www.opensource.org/licenses/mit-license.php. | # file COPYING or http://www.opensource.org/licenses/mit-license.php. | ||||
"""Run fuzz test targets. | """Run fuzz test targets. | ||||
""" | """ | ||||
import argparse | import argparse | ||||
from concurrent.futures import ThreadPoolExecutor, as_completed | |||||
import configparser | import configparser | ||||
import logging | import logging | ||||
import os | import os | ||||
import subprocess | import subprocess | ||||
import sys | import sys | ||||
def main(): | def main(): | ||||
Show All 14 Lines | parser.add_argument( | ||||
help='If true, run fuzzing binaries under the valgrind memory error detector', | help='If true, run fuzzing binaries under the valgrind memory error detector', | ||||
) | ) | ||||
parser.add_argument( | parser.add_argument( | ||||
'-x', | '-x', | ||||
'--exclude', | '--exclude', | ||||
help="A comma-separated list of targets to exclude", | help="A comma-separated list of targets to exclude", | ||||
) | ) | ||||
parser.add_argument( | parser.add_argument( | ||||
'--par', | |||||
type=int, | |||||
default=4, | |||||
help='How many targets to merge or execute in parallel.', | |||||
) | |||||
parser.add_argument( | |||||
'seed_dir', | 'seed_dir', | ||||
help='The seed corpus to run on (must contain subfolders for each fuzz target).', | help='The seed corpus to run on (must contain subfolders for each fuzz target).', | ||||
) | ) | ||||
parser.add_argument( | parser.add_argument( | ||||
'target', | 'target', | ||||
nargs='*', | nargs='*', | ||||
help='The target(s) to run. Default is to run all targets.', | help='The target(s) to run. Default is to run all targets.', | ||||
) | ) | ||||
▲ Show 20 Lines • Show All 92 Lines • ▼ Show 20 Lines | try: | ||||
if "libFuzzer" not in help_output: | if "libFuzzer" not in help_output: | ||||
logging.error("Must be built with libFuzzer") | logging.error("Must be built with libFuzzer") | ||||
sys.exit(1) | sys.exit(1) | ||||
except subprocess.TimeoutExpired: | except subprocess.TimeoutExpired: | ||||
logging.error( | logging.error( | ||||
"subprocess timed out: Currently only libFuzzer is supported") | "subprocess timed out: Currently only libFuzzer is supported") | ||||
sys.exit(1) | sys.exit(1) | ||||
with ThreadPoolExecutor(max_workers=args.par) as fuzz_pool: | |||||
if args.m_dir: | if args.m_dir: | ||||
merge_inputs( | merge_inputs( | ||||
fuzz_pool=fuzz_pool, | |||||
corpus=args.seed_dir, | corpus=args.seed_dir, | ||||
test_list=test_list_selection, | test_list=test_list_selection, | ||||
build_dir=config["environment"]["BUILDDIR"], | test_dir=test_dir, | ||||
merge_dir=args.m_dir, | merge_dir=args.m_dir, | ||||
) | ) | ||||
return | return | ||||
run_once( | run_once( | ||||
fuzz_pool=fuzz_pool, | |||||
corpus=args.seed_dir, | corpus=args.seed_dir, | ||||
test_list=test_list_selection, | test_list=test_list_selection, | ||||
test_dir=test_dir, | test_dir=test_dir, | ||||
use_valgrind=args.valgrind, | use_valgrind=args.valgrind, | ||||
) | ) | ||||
def merge_inputs(*, corpus, test_list, build_dir, merge_dir): | def merge_inputs(*, fuzz_pool, corpus, test_list, test_dir, merge_dir): | ||||
logging.info( | logging.info( | ||||
"Merge the inputs in the passed dir into the seed_dir. Passed dir {}".format(merge_dir)) | "Merge the inputs in the passed dir into the seed_dir. Passed dir {}".format(merge_dir)) | ||||
jobs = [] | |||||
for t in test_list: | for t in test_list: | ||||
args = [ | args = [ | ||||
os.path.join(build_dir, 'src', 'test', 'fuzz', t), | os.path.join(test_dir, t), | ||||
'-merge=1', | '-merge=1', | ||||
# Also done by oss-fuzz | # Also done by oss-fuzz | ||||
# https://github.com/google/oss-fuzz/issues/1406#issuecomment-387790487 | # https://github.com/google/oss-fuzz/issues/1406#issuecomment-387790487 | ||||
'-use_value_profile=1', | '-use_value_profile=1', | ||||
os.path.join(corpus, t), | os.path.join(corpus, t), | ||||
os.path.join(merge_dir, t), | os.path.join(merge_dir, t), | ||||
] | ] | ||||
os.makedirs(os.path.join(corpus, t), exist_ok=True) | os.makedirs(os.path.join(corpus, t), exist_ok=True) | ||||
os.makedirs(os.path.join(merge_dir, t), exist_ok=True) | os.makedirs(os.path.join(merge_dir, t), exist_ok=True) | ||||
logging.debug('Run {} with args {}'.format(t, args)) | |||||
output = subprocess.run( | def job(t, args): | ||||
args, | output = 'Run {} with args {}\n'.format(t, " ".join(args)) | ||||
output += subprocess.run(args, | |||||
check=True, | check=True, | ||||
stderr=subprocess.PIPE, | stderr=subprocess.PIPE, | ||||
universal_newlines=True).stderr | universal_newlines=True).stderr | ||||
logging.debug('Output: {}'.format(output)) | logging.debug(output) | ||||
jobs.append(fuzz_pool.submit(job, t, args)) | |||||
def run_once(*, corpus, test_list, test_dir, use_valgrind): | for future in as_completed(jobs): | ||||
future.result() | |||||
def run_once(*, fuzz_pool, corpus, test_list, test_dir, use_valgrind): | |||||
jobs = [] | |||||
for t in test_list: | for t in test_list: | ||||
corpus_path = os.path.join(corpus, t) | corpus_path = os.path.join(corpus, t) | ||||
os.makedirs(corpus_path, exist_ok=True) | os.makedirs(corpus_path, exist_ok=True) | ||||
args = [ | args = [ | ||||
os.path.join(test_dir, t), | os.path.join(test_dir, t), | ||||
'-runs=1', | '-runs=1', | ||||
corpus_path, | corpus_path, | ||||
] | ] | ||||
if use_valgrind: | if use_valgrind: | ||||
args = [ | args = [ | ||||
'valgrind', | 'valgrind', | ||||
'--quiet', | '--quiet', | ||||
'--error-exitcode=1'] + args | '--error-exitcode=1'] + args | ||||
logging.debug('Run {} with args {}'.format(t, args)) | |||||
def job(t, args): | |||||
output = 'Run {} with args {}'.format(t, args) | |||||
result = subprocess.run( | result = subprocess.run( | ||||
args, | args, | ||||
stderr=subprocess.PIPE, | stderr=subprocess.PIPE, | ||||
universal_newlines=True) | universal_newlines=True) | ||||
output = result.stderr | output += result.stderr | ||||
logging.debug('Output: {}'.format(output)) | return output, result | ||||
jobs.append(fuzz_pool.submit(job, t, args)) | |||||
for future in as_completed(jobs): | |||||
output, result = future.result() | |||||
logging.debug(output) | |||||
try: | try: | ||||
result.check_returncode() | result.check_returncode() | ||||
except subprocess.CalledProcessError as e: | except subprocess.CalledProcessError as e: | ||||
if e.stdout: | if e.stdout: | ||||
logging.info(e.stdout) | logging.info(e.stdout) | ||||
if e.stderr: | if e.stderr: | ||||
logging.info(e.stderr) | logging.info(e.stderr) | ||||
logging.info( | logging.info( | ||||
"Target \"{}\" failed with exit code {}: {}".format( | "Target \"{}\" failed with exit code {}".format( | ||||
t, e.returncode, " ".join(args))) | " ".join( | ||||
result.args), | |||||
e.returncode)) | |||||
sys.exit(1) | sys.exit(1) | ||||
if __name__ == '__main__': | if __name__ == '__main__': | ||||
main() | main() |