| #!/usr/bin/env python | |
| # ################################################################ | |
| # Copyright (c) 2016-present, Facebook, Inc. | |
| # All rights reserved. | |
| # | |
| # This source code is licensed under both the BSD-style license (found in the | |
| # LICENSE file in the root directory of this source tree) and the GPLv2 (found | |
| # in the COPYING file in the root directory of this source tree). | |
| # ########################################################################## | |
| import argparse | |
| import contextlib | |
| import os | |
| import re | |
| import shlex | |
| import shutil | |
| import subprocess | |
| import sys | |
| import tempfile | |
| def abs_join(a, *p): | |
| return os.path.abspath(os.path.join(a, *p)) | |
| class InputType(object): | |
| RAW_DATA = 1 | |
| COMPRESSED_DATA = 2 | |
| class FrameType(object): | |
| ZSTD = 1 | |
| BLOCK = 2 | |
| class TargetInfo(object): | |
| def __init__(self, input_type, frame_type=FrameType.ZSTD): | |
| self.input_type = input_type | |
| self.frame_type = frame_type | |
| # Constants | |
| FUZZ_DIR = os.path.abspath(os.path.dirname(__file__)) | |
| CORPORA_DIR = abs_join(FUZZ_DIR, 'corpora') | |
| TARGET_INFO = { | |
| 'simple_round_trip': TargetInfo(InputType.RAW_DATA), | |
| 'stream_round_trip': TargetInfo(InputType.RAW_DATA), | |
| 'block_round_trip': TargetInfo(InputType.RAW_DATA, FrameType.BLOCK), | |
| 'simple_decompress': TargetInfo(InputType.COMPRESSED_DATA), | |
| 'stream_decompress': TargetInfo(InputType.COMPRESSED_DATA), | |
| 'block_decompress': TargetInfo(InputType.COMPRESSED_DATA, FrameType.BLOCK), | |
| 'dictionary_round_trip': TargetInfo(InputType.RAW_DATA), | |
| 'dictionary_decompress': TargetInfo(InputType.COMPRESSED_DATA), | |
| 'zstd_frame_info': TargetInfo(InputType.COMPRESSED_DATA), | |
| 'simple_compress': TargetInfo(InputType.RAW_DATA), | |
| } | |
| TARGETS = list(TARGET_INFO.keys()) | |
| ALL_TARGETS = TARGETS + ['all'] | |
| FUZZ_RNG_SEED_SIZE = 4 | |
| # Standard environment variables | |
| CC = os.environ.get('CC', 'cc') | |
| CXX = os.environ.get('CXX', 'c++') | |
| CPPFLAGS = os.environ.get('CPPFLAGS', '') | |
| CFLAGS = os.environ.get('CFLAGS', '-O3') | |
| CXXFLAGS = os.environ.get('CXXFLAGS', CFLAGS) | |
| LDFLAGS = os.environ.get('LDFLAGS', '') | |
| MFLAGS = os.environ.get('MFLAGS', '-j') | |
| # Fuzzing environment variables | |
| LIB_FUZZING_ENGINE = os.environ.get('LIB_FUZZING_ENGINE', 'libregression.a') | |
| AFL_FUZZ = os.environ.get('AFL_FUZZ', 'afl-fuzz') | |
| DECODECORPUS = os.environ.get('DECODECORPUS', | |
| abs_join(FUZZ_DIR, '..', 'decodecorpus')) | |
| # Sanitizer environment variables | |
| MSAN_EXTRA_CPPFLAGS = os.environ.get('MSAN_EXTRA_CPPFLAGS', '') | |
| MSAN_EXTRA_CFLAGS = os.environ.get('MSAN_EXTRA_CFLAGS', '') | |
| MSAN_EXTRA_CXXFLAGS = os.environ.get('MSAN_EXTRA_CXXFLAGS', '') | |
| MSAN_EXTRA_LDFLAGS = os.environ.get('MSAN_EXTRA_LDFLAGS', '') | |
| def create(r): | |
| d = os.path.abspath(r) | |
| if not os.path.isdir(d): | |
| os.makedirs(d) | |
| return d | |
| def check(r): | |
| d = os.path.abspath(r) | |
| if not os.path.isdir(d): | |
| return None | |
| return d | |
| @contextlib.contextmanager | |
| def tmpdir(): | |
| dirpath = tempfile.mkdtemp() | |
| try: | |
| yield dirpath | |
| finally: | |
| shutil.rmtree(dirpath, ignore_errors=True) | |
| def parse_targets(in_targets): | |
| targets = set() | |
| for target in in_targets: | |
| if not target: | |
| continue | |
| if target == 'all': | |
| targets = targets.union(TARGETS) | |
| elif target in TARGETS: | |
| targets.add(target) | |
| else: | |
| raise RuntimeError('{} is not a valid target'.format(target)) | |
| return list(targets) | |
| def targets_parser(args, description): | |
| parser = argparse.ArgumentParser(prog=args.pop(0), description=description) | |
| parser.add_argument( | |
| 'TARGET', | |
| nargs='*', | |
| type=str, | |
| help='Fuzz target(s) to build {{{}}}'.format(', '.join(ALL_TARGETS))) | |
| args, extra = parser.parse_known_args(args) | |
| args.extra = extra | |
| args.TARGET = parse_targets(args.TARGET) | |
| return args | |
| def parse_env_flags(args, flags): | |
| """ | |
| Look for flags set by environment variables. | |
| """ | |
| san_flags = ','.join(re.findall('-fsanitize=((?:[a-z]+,?)+)', flags)) | |
| nosan_flags = ','.join(re.findall('-fno-sanitize=((?:[a-z]+,?)+)', flags)) | |
| def set_sanitizer(sanitizer, default, san, nosan): | |
| if sanitizer in san and sanitizer in nosan: | |
| raise RuntimeError('-fno-sanitize={s} and -fsanitize={s} passed'. | |
| format(s=sanitizer)) | |
| if sanitizer in san: | |
| return True | |
| if sanitizer in nosan: | |
| return False | |
| return default | |
| san = set(san_flags.split(',')) | |
| nosan = set(nosan_flags.split(',')) | |
| args.asan = set_sanitizer('address', args.asan, san, nosan) | |
| args.msan = set_sanitizer('memory', args.msan, san, nosan) | |
| args.ubsan = set_sanitizer('undefined', args.ubsan, san, nosan) | |
| args.sanitize = args.asan or args.msan or args.ubsan | |
| return args | |
| def compiler_version(cc, cxx): | |
| """ | |
| Determines the compiler and version. | |
| Only works for clang and gcc. | |
| """ | |
| cc_version_bytes = subprocess.check_output([cc, "--version"]) | |
| cxx_version_bytes = subprocess.check_output([cxx, "--version"]) | |
| compiler = None | |
| version = None | |
| if b'clang' in cc_version_bytes: | |
| assert(b'clang' in cxx_version_bytes) | |
| compiler = 'clang' | |
| elif b'gcc' in cc_version_bytes: | |
| assert(b'gcc' in cxx_version_bytes or b'g++' in cxx_version_bytes) | |
| compiler = 'gcc' | |
| if compiler is not None: | |
| version_regex = b'([0-9])+\.([0-9])+\.([0-9])+' | |
| version_match = re.search(version_regex, cc_version_bytes) | |
| version = tuple(int(version_match.group(i)) for i in range(1, 4)) | |
| return compiler, version | |
| def overflow_ubsan_flags(cc, cxx): | |
| compiler, version = compiler_version(cc, cxx) | |
| if compiler == 'gcc': | |
| return ['-fno-sanitize=signed-integer-overflow'] | |
| if compiler == 'clang' and version >= (5, 0, 0): | |
| return ['-fno-sanitize=pointer-overflow'] | |
| return [] | |
| def build_parser(args): | |
| description = """ | |
| Cleans the repository and builds a fuzz target (or all). | |
| Many flags default to environment variables (default says $X='y'). | |
| Options that aren't enabling features default to the correct values for | |
| zstd. | |
| Enable sanitizers with --enable-*san. | |
| For regression testing just build. | |
| For libFuzzer set LIB_FUZZING_ENGINE and pass --enable-coverage. | |
| For AFL set CC and CXX to AFL's compilers and set | |
| LIB_FUZZING_ENGINE='libregression.a'. | |
| """ | |
| parser = argparse.ArgumentParser(prog=args.pop(0), description=description) | |
| parser.add_argument( | |
| '--lib-fuzzing-engine', | |
| dest='lib_fuzzing_engine', | |
| type=str, | |
| default=LIB_FUZZING_ENGINE, | |
| help=('The fuzzing engine to use e.g. /path/to/libFuzzer.a ' | |
| "(default: $LIB_FUZZING_ENGINE='{})".format(LIB_FUZZING_ENGINE))) | |
| fuzz_group = parser.add_mutually_exclusive_group() | |
| fuzz_group.add_argument( | |
| '--enable-coverage', | |
| dest='coverage', | |
| action='store_true', | |
| help='Enable coverage instrumentation (-fsanitize-coverage)') | |
| fuzz_group.add_argument( | |
| '--enable-fuzzer', | |
| dest='fuzzer', | |
| action='store_true', | |
| help=('Enable clang fuzzer (-fsanitize=fuzzer). When enabled ' | |
| 'LIB_FUZZING_ENGINE is ignored') | |
| ) | |
| parser.add_argument( | |
| '--enable-asan', dest='asan', action='store_true', help='Enable UBSAN') | |
| parser.add_argument( | |
| '--enable-ubsan', | |
| dest='ubsan', | |
| action='store_true', | |
| help='Enable UBSAN') | |
| parser.add_argument( | |
| '--enable-ubsan-pointer-overflow', | |
| dest='ubsan_pointer_overflow', | |
| action='store_true', | |
| help='Enable UBSAN pointer overflow check (known failure)') | |
| parser.add_argument( | |
| '--enable-msan', dest='msan', action='store_true', help='Enable MSAN') | |
| parser.add_argument( | |
| '--enable-msan-track-origins', dest='msan_track_origins', | |
| action='store_true', help='Enable MSAN origin tracking') | |
| parser.add_argument( | |
| '--msan-extra-cppflags', | |
| dest='msan_extra_cppflags', | |
| type=str, | |
| default=MSAN_EXTRA_CPPFLAGS, | |
| help="Extra CPPFLAGS for MSAN (default: $MSAN_EXTRA_CPPFLAGS='{}')". | |
| format(MSAN_EXTRA_CPPFLAGS)) | |
| parser.add_argument( | |
| '--msan-extra-cflags', | |
| dest='msan_extra_cflags', | |
| type=str, | |
| default=MSAN_EXTRA_CFLAGS, | |
| help="Extra CFLAGS for MSAN (default: $MSAN_EXTRA_CFLAGS='{}')".format( | |
| MSAN_EXTRA_CFLAGS)) | |
| parser.add_argument( | |
| '--msan-extra-cxxflags', | |
| dest='msan_extra_cxxflags', | |
| type=str, | |
| default=MSAN_EXTRA_CXXFLAGS, | |
| help="Extra CXXFLAGS for MSAN (default: $MSAN_EXTRA_CXXFLAGS='{}')". | |
| format(MSAN_EXTRA_CXXFLAGS)) | |
| parser.add_argument( | |
| '--msan-extra-ldflags', | |
| dest='msan_extra_ldflags', | |
| type=str, | |
| default=MSAN_EXTRA_LDFLAGS, | |
| help="Extra LDFLAGS for MSAN (default: $MSAN_EXTRA_LDFLAGS='{}')". | |
| format(MSAN_EXTRA_LDFLAGS)) | |
| parser.add_argument( | |
| '--enable-sanitize-recover', | |
| dest='sanitize_recover', | |
| action='store_true', | |
| help='Non-fatal sanitizer errors where possible') | |
| parser.add_argument( | |
| '--debug', | |
| dest='debug', | |
| type=int, | |
| default=1, | |
| help='Set DEBUGLEVEL (default: 1)') | |
| parser.add_argument( | |
| '--force-memory-access', | |
| dest='memory_access', | |
| type=int, | |
| default=0, | |
| help='Set MEM_FORCE_MEMORY_ACCESS (default: 0)') | |
| parser.add_argument( | |
| '--fuzz-rng-seed-size', | |
| dest='fuzz_rng_seed_size', | |
| type=int, | |
| default=4, | |
| help='Set FUZZ_RNG_SEED_SIZE (default: 4)') | |
| parser.add_argument( | |
| '--disable-fuzzing-mode', | |
| dest='fuzzing_mode', | |
| action='store_false', | |
| help='Do not define FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION') | |
| parser.add_argument( | |
| '--enable-stateful-fuzzing', | |
| dest='stateful_fuzzing', | |
| action='store_true', | |
| help='Reuse contexts between runs (makes reproduction impossible)') | |
| parser.add_argument( | |
| '--cc', | |
| dest='cc', | |
| type=str, | |
| default=CC, | |
| help="CC (default: $CC='{}')".format(CC)) | |
| parser.add_argument( | |
| '--cxx', | |
| dest='cxx', | |
| type=str, | |
| default=CXX, | |
| help="CXX (default: $CXX='{}')".format(CXX)) | |
| parser.add_argument( | |
| '--cppflags', | |
| dest='cppflags', | |
| type=str, | |
| default=CPPFLAGS, | |
| help="CPPFLAGS (default: $CPPFLAGS='{}')".format(CPPFLAGS)) | |
| parser.add_argument( | |
| '--cflags', | |
| dest='cflags', | |
| type=str, | |
| default=CFLAGS, | |
| help="CFLAGS (default: $CFLAGS='{}')".format(CFLAGS)) | |
| parser.add_argument( | |
| '--cxxflags', | |
| dest='cxxflags', | |
| type=str, | |
| default=CXXFLAGS, | |
| help="CXXFLAGS (default: $CXXFLAGS='{}')".format(CXXFLAGS)) | |
| parser.add_argument( | |
| '--ldflags', | |
| dest='ldflags', | |
| type=str, | |
| default=LDFLAGS, | |
| help="LDFLAGS (default: $LDFLAGS='{}')".format(LDFLAGS)) | |
| parser.add_argument( | |
| '--mflags', | |
| dest='mflags', | |
| type=str, | |
| default=MFLAGS, | |
| help="Extra Make flags (default: $MFLAGS='{}')".format(MFLAGS)) | |
| parser.add_argument( | |
| 'TARGET', | |
| nargs='*', | |
| type=str, | |
| help='Fuzz target(s) to build {{{}}}'.format(', '.join(ALL_TARGETS)) | |
| ) | |
| args = parser.parse_args(args) | |
| args = parse_env_flags(args, ' '.join( | |
| [args.cppflags, args.cflags, args.cxxflags, args.ldflags])) | |
| # Check option sanity | |
| if args.msan and (args.asan or args.ubsan): | |
| raise RuntimeError('MSAN may not be used with any other sanitizers') | |
| if args.msan_track_origins and not args.msan: | |
| raise RuntimeError('--enable-msan-track-origins requires MSAN') | |
| if args.ubsan_pointer_overflow and not args.ubsan: | |
| raise RuntimeError('--enable-ubsan-pointer-overflow requires UBSAN') | |
| if args.sanitize_recover and not args.sanitize: | |
| raise RuntimeError('--enable-sanitize-recover but no sanitizers used') | |
| return args | |
| def build(args): | |
| try: | |
| args = build_parser(args) | |
| except Exception as e: | |
| print(e) | |
| return 1 | |
| # The compilation flags we are setting | |
| targets = args.TARGET | |
| cc = args.cc | |
| cxx = args.cxx | |
| cppflags = shlex.split(args.cppflags) | |
| cflags = shlex.split(args.cflags) | |
| ldflags = shlex.split(args.ldflags) | |
| cxxflags = shlex.split(args.cxxflags) | |
| mflags = shlex.split(args.mflags) | |
| # Flags to be added to both cflags and cxxflags | |
| common_flags = [] | |
| cppflags += [ | |
| '-DDEBUGLEVEL={}'.format(args.debug), | |
| '-DMEM_FORCE_MEMORY_ACCESS={}'.format(args.memory_access), | |
| '-DFUZZ_RNG_SEED_SIZE={}'.format(args.fuzz_rng_seed_size), | |
| ] | |
| # Set flags for options | |
| assert not (args.fuzzer and args.coverage) | |
| if args.coverage: | |
| common_flags += [ | |
| '-fsanitize-coverage=trace-pc-guard,indirect-calls,trace-cmp' | |
| ] | |
| if args.fuzzer: | |
| common_flags += ['-fsanitize=fuzzer'] | |
| args.lib_fuzzing_engine = '' | |
| mflags += ['LIB_FUZZING_ENGINE={}'.format(args.lib_fuzzing_engine)] | |
| if args.sanitize_recover: | |
| recover_flags = ['-fsanitize-recover=all'] | |
| else: | |
| recover_flags = ['-fno-sanitize-recover=all'] | |
| if args.sanitize: | |
| common_flags += recover_flags | |
| if args.msan: | |
| msan_flags = ['-fsanitize=memory'] | |
| if args.msan_track_origins: | |
| msan_flags += ['-fsanitize-memory-track-origins'] | |
| common_flags += msan_flags | |
| # Append extra MSAN flags (it might require special setup) | |
| cppflags += [args.msan_extra_cppflags] | |
| cflags += [args.msan_extra_cflags] | |
| cxxflags += [args.msan_extra_cxxflags] | |
| ldflags += [args.msan_extra_ldflags] | |
| if args.asan: | |
| common_flags += ['-fsanitize=address'] | |
| if args.ubsan: | |
| ubsan_flags = ['-fsanitize=undefined'] | |
| if not args.ubsan_pointer_overflow: | |
| ubsan_flags += overflow_ubsan_flags(cc, cxx) | |
| common_flags += ubsan_flags | |
| if args.stateful_fuzzing: | |
| cppflags += ['-DSTATEFUL_FUZZING'] | |
| if args.fuzzing_mode: | |
| cppflags += ['-DFUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION'] | |
| if args.lib_fuzzing_engine == 'libregression.a': | |
| targets = ['libregression.a'] + targets | |
| # Append the common flags | |
| cflags += common_flags | |
| cxxflags += common_flags | |
| # Prepare the flags for Make | |
| cc_str = "CC={}".format(cc) | |
| cxx_str = "CXX={}".format(cxx) | |
| cppflags_str = "CPPFLAGS={}".format(' '.join(cppflags)) | |
| cflags_str = "CFLAGS={}".format(' '.join(cflags)) | |
| cxxflags_str = "CXXFLAGS={}".format(' '.join(cxxflags)) | |
| ldflags_str = "LDFLAGS={}".format(' '.join(ldflags)) | |
| # Print the flags | |
| print('MFLAGS={}'.format(' '.join(mflags))) | |
| print(cc_str) | |
| print(cxx_str) | |
| print(cppflags_str) | |
| print(cflags_str) | |
| print(cxxflags_str) | |
| print(ldflags_str) | |
| # Clean and build | |
| clean_cmd = ['make', 'clean'] + mflags | |
| print(' '.join(clean_cmd)) | |
| subprocess.check_call(clean_cmd) | |
| build_cmd = [ | |
| 'make', | |
| cc_str, | |
| cxx_str, | |
| cppflags_str, | |
| cflags_str, | |
| cxxflags_str, | |
| ldflags_str, | |
| ] + mflags + targets | |
| print(' '.join(build_cmd)) | |
| subprocess.check_call(build_cmd) | |
| return 0 | |
| def libfuzzer_parser(args): | |
| description = """ | |
| Runs a libfuzzer binary. | |
| Passes all extra arguments to libfuzzer. | |
| The fuzzer should have been build with LIB_FUZZING_ENGINE pointing to | |
| libFuzzer.a. | |
| Generates output in the CORPORA directory, puts crashes in the ARTIFACT | |
| directory, and takes extra input from the SEED directory. | |
| To merge AFL's output pass the SEED as AFL's output directory and pass | |
| '-merge=1'. | |
| """ | |
| parser = argparse.ArgumentParser(prog=args.pop(0), description=description) | |
| parser.add_argument( | |
| '--corpora', | |
| type=str, | |
| help='Override the default corpora dir (default: {})'.format( | |
| abs_join(CORPORA_DIR, 'TARGET'))) | |
| parser.add_argument( | |
| '--artifact', | |
| type=str, | |
| help='Override the default artifact dir (default: {})'.format( | |
| abs_join(CORPORA_DIR, 'TARGET-crash'))) | |
| parser.add_argument( | |
| '--seed', | |
| type=str, | |
| help='Override the default seed dir (default: {})'.format( | |
| abs_join(CORPORA_DIR, 'TARGET-seed'))) | |
| parser.add_argument( | |
| 'TARGET', | |
| type=str, | |
| help='Fuzz target(s) to build {{{}}}'.format(', '.join(TARGETS))) | |
| args, extra = parser.parse_known_args(args) | |
| args.extra = extra | |
| if args.TARGET and args.TARGET not in TARGETS: | |
| raise RuntimeError('{} is not a valid target'.format(args.TARGET)) | |
| return args | |
| def libfuzzer(target, corpora=None, artifact=None, seed=None, extra_args=None): | |
| if corpora is None: | |
| corpora = abs_join(CORPORA_DIR, target) | |
| if artifact is None: | |
| artifact = abs_join(CORPORA_DIR, '{}-crash'.format(target)) | |
| if seed is None: | |
| seed = abs_join(CORPORA_DIR, '{}-seed'.format(target)) | |
| if extra_args is None: | |
| extra_args = [] | |
| target = abs_join(FUZZ_DIR, target) | |
| corpora = [create(corpora)] | |
| artifact = create(artifact) | |
| seed = check(seed) | |
| corpora += [artifact] | |
| if seed is not None: | |
| corpora += [seed] | |
| cmd = [target, '-artifact_prefix={}/'.format(artifact)] | |
| cmd += corpora + extra_args | |
| print(' '.join(cmd)) | |
| subprocess.check_call(cmd) | |
| def libfuzzer_cmd(args): | |
| try: | |
| args = libfuzzer_parser(args) | |
| except Exception as e: | |
| print(e) | |
| return 1 | |
| libfuzzer(args.TARGET, args.corpora, args.artifact, args.seed, args.extra) | |
| return 0 | |
| def afl_parser(args): | |
| description = """ | |
| Runs an afl-fuzz job. | |
| Passes all extra arguments to afl-fuzz. | |
| The fuzzer should have been built with CC/CXX set to the AFL compilers, | |
| and with LIB_FUZZING_ENGINE='libregression.a'. | |
| Takes input from CORPORA and writes output to OUTPUT. | |
| Uses AFL_FUZZ as the binary (set from flag or environment variable). | |
| """ | |
| parser = argparse.ArgumentParser(prog=args.pop(0), description=description) | |
| parser.add_argument( | |
| '--corpora', | |
| type=str, | |
| help='Override the default corpora dir (default: {})'.format( | |
| abs_join(CORPORA_DIR, 'TARGET'))) | |
| parser.add_argument( | |
| '--output', | |
| type=str, | |
| help='Override the default AFL output dir (default: {})'.format( | |
| abs_join(CORPORA_DIR, 'TARGET-afl'))) | |
| parser.add_argument( | |
| '--afl-fuzz', | |
| type=str, | |
| default=AFL_FUZZ, | |
| help='AFL_FUZZ (default: $AFL_FUZZ={})'.format(AFL_FUZZ)) | |
| parser.add_argument( | |
| 'TARGET', | |
| type=str, | |
| help='Fuzz target(s) to build {{{}}}'.format(', '.join(TARGETS))) | |
| args, extra = parser.parse_known_args(args) | |
| args.extra = extra | |
| if args.TARGET and args.TARGET not in TARGETS: | |
| raise RuntimeError('{} is not a valid target'.format(args.TARGET)) | |
| if not args.corpora: | |
| args.corpora = abs_join(CORPORA_DIR, args.TARGET) | |
| if not args.output: | |
| args.output = abs_join(CORPORA_DIR, '{}-afl'.format(args.TARGET)) | |
| return args | |
| def afl(args): | |
| try: | |
| args = afl_parser(args) | |
| except Exception as e: | |
| print(e) | |
| return 1 | |
| target = abs_join(FUZZ_DIR, args.TARGET) | |
| corpora = create(args.corpora) | |
| output = create(args.output) | |
| cmd = [args.afl_fuzz, '-i', corpora, '-o', output] + args.extra | |
| cmd += [target, '@@'] | |
| print(' '.join(cmd)) | |
| subprocess.call(cmd) | |
| return 0 | |
| def regression(args): | |
| try: | |
| description = """ | |
| Runs one or more regression tests. | |
| The fuzzer should have been built with with | |
| LIB_FUZZING_ENGINE='libregression.a'. | |
| Takes input from CORPORA. | |
| """ | |
| args = targets_parser(args, description) | |
| except Exception as e: | |
| print(e) | |
| return 1 | |
| for target in args.TARGET: | |
| corpora = create(abs_join(CORPORA_DIR, target)) | |
| target = abs_join(FUZZ_DIR, target) | |
| cmd = [target, corpora] | |
| print(' '.join(cmd)) | |
| subprocess.check_call(cmd) | |
| return 0 | |
| def gen_parser(args): | |
| description = """ | |
| Generate a seed corpus appropriate for TARGET with data generated with | |
| decodecorpus. | |
| The fuzz inputs are prepended with a seed before the zstd data, so the | |
| output of decodecorpus shouldn't be used directly. | |
| Generates NUMBER samples prepended with FUZZ_RNG_SEED_SIZE random bytes and | |
| puts the output in SEED. | |
| DECODECORPUS is the decodecorpus binary, and must already be built. | |
| """ | |
| parser = argparse.ArgumentParser(prog=args.pop(0), description=description) | |
| parser.add_argument( | |
| '--number', | |
| '-n', | |
| type=int, | |
| default=100, | |
| help='Number of samples to generate') | |
| parser.add_argument( | |
| '--max-size-log', | |
| type=int, | |
| default=18, | |
| help='Maximum sample size to generate') | |
| parser.add_argument( | |
| '--seed', | |
| type=str, | |
| help='Override the default seed dir (default: {})'.format( | |
| abs_join(CORPORA_DIR, 'TARGET-seed'))) | |
| parser.add_argument( | |
| '--decodecorpus', | |
| type=str, | |
| default=DECODECORPUS, | |
| help="decodecorpus binary (default: $DECODECORPUS='{}')".format( | |
| DECODECORPUS)) | |
| parser.add_argument( | |
| '--fuzz-rng-seed-size', | |
| type=int, | |
| default=4, | |
| help="FUZZ_RNG_SEED_SIZE used for generate the samples (must match)" | |
| ) | |
| parser.add_argument( | |
| 'TARGET', | |
| type=str, | |
| help='Fuzz target(s) to build {{{}}}'.format(', '.join(TARGETS))) | |
| args, extra = parser.parse_known_args(args) | |
| args.extra = extra | |
| if args.TARGET and args.TARGET not in TARGETS: | |
| raise RuntimeError('{} is not a valid target'.format(args.TARGET)) | |
| if not args.seed: | |
| args.seed = abs_join(CORPORA_DIR, '{}-seed'.format(args.TARGET)) | |
| if not os.path.isfile(args.decodecorpus): | |
| raise RuntimeError("{} is not a file run 'make -C {} decodecorpus'". | |
| format(args.decodecorpus, abs_join(FUZZ_DIR, '..'))) | |
| return args | |
| def gen(args): | |
| try: | |
| args = gen_parser(args) | |
| except Exception as e: | |
| print(e) | |
| return 1 | |
| seed = create(args.seed) | |
| with tmpdir() as compressed: | |
| with tmpdir() as decompressed: | |
| cmd = [ | |
| args.decodecorpus, | |
| '-n{}'.format(args.number), | |
| '-p{}/'.format(compressed), | |
| '-o{}'.format(decompressed), | |
| ] | |
| info = TARGET_INFO[args.TARGET] | |
| if info.frame_type == FrameType.BLOCK: | |
| cmd += [ | |
| '--gen-blocks', | |
| '--max-block-size-log={}'.format(min(args.max_size_log, 17)) | |
| ] | |
| else: | |
| cmd += ['--max-content-size-log={}'.format(args.max_size_log)] | |
| print(' '.join(cmd)) | |
| subprocess.check_call(cmd) | |
| if info.input_type == InputType.RAW_DATA: | |
| print('using decompressed data in {}'.format(decompressed)) | |
| samples = decompressed | |
| else: | |
| assert info.input_type == InputType.COMPRESSED_DATA | |
| print('using compressed data in {}'.format(compressed)) | |
| samples = compressed | |
| # Copy the samples over and prepend the RNG seeds | |
| for name in os.listdir(samples): | |
| samplename = abs_join(samples, name) | |
| outname = abs_join(seed, name) | |
| with open(samplename, 'rb') as sample: | |
| with open(outname, 'wb') as out: | |
| CHUNK_SIZE = 131072 | |
| chunk = sample.read(CHUNK_SIZE) | |
| while len(chunk) > 0: | |
| out.write(chunk) | |
| chunk = sample.read(CHUNK_SIZE) | |
| return 0 | |
| def minimize(args): | |
| try: | |
| description = """ | |
| Runs a libfuzzer fuzzer with -merge=1 to build a minimal corpus in | |
| TARGET_seed_corpus. All extra args are passed to libfuzzer. | |
| """ | |
| args = targets_parser(args, description) | |
| except Exception as e: | |
| print(e) | |
| return 1 | |
| for target in args.TARGET: | |
| # Merge the corpus + anything else into the seed_corpus | |
| corpus = abs_join(CORPORA_DIR, target) | |
| seed_corpus = abs_join(CORPORA_DIR, "{}_seed_corpus".format(target)) | |
| extra_args = [corpus, "-merge=1"] + args.extra | |
| libfuzzer(target, corpora=seed_corpus, extra_args=extra_args) | |
| seeds = set(os.listdir(seed_corpus)) | |
| # Copy all crashes directly into the seed_corpus if not already present | |
| crashes = abs_join(CORPORA_DIR, '{}-crash'.format(target)) | |
| for crash in os.listdir(crashes): | |
| if crash not in seeds: | |
| shutil.copy(abs_join(crashes, crash), seed_corpus) | |
| seeds.add(crash) | |
| def zip_cmd(args): | |
| try: | |
| description = """ | |
| Zips up the seed corpus. | |
| """ | |
| args = targets_parser(args, description) | |
| except Exception as e: | |
| print(e) | |
| return 1 | |
| for target in args.TARGET: | |
| # Zip the seed_corpus | |
| seed_corpus = abs_join(CORPORA_DIR, "{}_seed_corpus".format(target)) | |
| zip_file = "{}.zip".format(seed_corpus) | |
| cmd = ["zip", "-r", "-q", "-j", "-9", zip_file, "."] | |
| print(' '.join(cmd)) | |
| subprocess.check_call(cmd, cwd=seed_corpus) | |
| def list_cmd(args): | |
| print("\n".join(TARGETS)) | |
| def short_help(args): | |
| name = args[0] | |
| print("Usage: {} [OPTIONS] COMMAND [ARGS]...\n".format(name)) | |
| def help(args): | |
| short_help(args) | |
| print("\tfuzzing helpers (select a command and pass -h for help)\n") | |
| print("Options:") | |
| print("\t-h, --help\tPrint this message") | |
| print("") | |
| print("Commands:") | |
| print("\tbuild\t\tBuild a fuzzer") | |
| print("\tlibfuzzer\tRun a libFuzzer fuzzer") | |
| print("\tafl\t\tRun an AFL fuzzer") | |
| print("\tregression\tRun a regression test") | |
| print("\tgen\t\tGenerate a seed corpus for a fuzzer") | |
| print("\tminimize\tMinimize the test corpora") | |
| print("\tzip\t\tZip the minimized corpora up") | |
| print("\tlist\t\tList the available targets") | |
| def main(): | |
| args = sys.argv | |
| if len(args) < 2: | |
| help(args) | |
| return 1 | |
| if args[1] == '-h' or args[1] == '--help' or args[1] == '-H': | |
| help(args) | |
| return 1 | |
| command = args.pop(1) | |
| args[0] = "{} {}".format(args[0], command) | |
| if command == "build": | |
| return build(args) | |
| if command == "libfuzzer": | |
| return libfuzzer_cmd(args) | |
| if command == "regression": | |
| return regression(args) | |
| if command == "afl": | |
| return afl(args) | |
| if command == "gen": | |
| return gen(args) | |
| if command == "minimize": | |
| return minimize(args) | |
| if command == "zip": | |
| return zip_cmd(args) | |
| if command == "list": | |
| return list_cmd(args) | |
| short_help(args) | |
| print("Error: No such command {} (pass -h for help)".format(command)) | |
| return 1 | |
| if __name__ == "__main__": | |
| sys.exit(main()) |