3 # ################################################################
4 # Copyright (c) 2016-present, Facebook, Inc.
7 # This source code is licensed under both the BSD-style license (found in the
8 # LICENSE file in the root directory of this source tree) and the GPLv2 (found
9 # in the COPYING file in the root directory of this source tree).
10 # ##########################################################################
23 return os.path.abspath(os.path.join(a, *p))
27 FUZZ_DIR = os.path.abspath(os.path.dirname(__file__))
28 CORPORA_DIR = abs_join(FUZZ_DIR, 'corpora')
37 ALL_TARGETS = TARGETS + ['all']
38 FUZZ_RNG_SEED_SIZE = 4
40 # Standard environment variables
41 CC = os.environ.get('CC', 'cc')
42 CXX = os.environ.get('CXX', 'c++')
43 CPPFLAGS = os.environ.get('CPPFLAGS', '')
44 CFLAGS = os.environ.get('CFLAGS', '-O3')
45 CXXFLAGS = os.environ.get('CXXFLAGS', CFLAGS)
46 LDFLAGS = os.environ.get('LDFLAGS', '')
47 MFLAGS = os.environ.get('MFLAGS', '-j')
49 # Fuzzing environment variables
50 LIB_FUZZING_ENGINE = os.environ.get('LIB_FUZZING_ENGINE', 'libregression.a')
51 AFL_FUZZ = os.environ.get('AFL_FUZZ', 'afl-fuzz')
52 DECODECORPUS = os.environ.get('DECODECORPUS',
53 abs_join(FUZZ_DIR, '..', 'decodecorpus'))
55 # Sanitizer environment variables
56 MSAN_EXTRA_CPPFLAGS = os.environ.get('MSAN_EXTRA_CPPFLAGS', '')
57 MSAN_EXTRA_CFLAGS = os.environ.get('MSAN_EXTRA_CFLAGS', '')
58 MSAN_EXTRA_CXXFLAGS = os.environ.get('MSAN_EXTRA_CXXFLAGS', '')
59 MSAN_EXTRA_LDFLAGS = os.environ.get('MSAN_EXTRA_LDFLAGS', '')
63 d = os.path.abspath(r)
64 if not os.path.isdir(d):
70 d = os.path.abspath(r)
71 if not os.path.isdir(d):
76 @contextlib.contextmanager
78 dirpath = tempfile.mkdtemp()
82 shutil.rmtree(dirpath, ignore_errors=True)
85 def parse_targets(in_targets):
87 for target in in_targets:
91 targets = targets.union(TARGETS)
92 elif target in TARGETS:
95 raise RuntimeError('{} is not a valid target'.format(target))
99 def targets_parser(args, description):
100 parser = argparse.ArgumentParser(prog=args.pop(0), description=description)
105 help='Fuzz target(s) to build {{{}}}'.format(', '.join(ALL_TARGETS)))
106 args, extra = parser.parse_known_args(args)
109 args.TARGET = parse_targets(args.TARGET)
114 def parse_env_flags(args, flags):
116 Look for flags set by environment variables.
118 san_flags = ','.join(re.findall('-fsanitize=((?:[a-z]+,?)+)', flags))
119 nosan_flags = ','.join(re.findall('-fno-sanitize=((?:[a-z]+,?)+)', flags))
121 def set_sanitizer(sanitizer, default, san, nosan):
122 if sanitizer in san and sanitizer in nosan:
123 raise RuntimeError('-fno-sanitize={s} and -fsanitize={s} passed'.
127 if sanitizer in nosan:
131 san = set(san_flags.split(','))
132 nosan = set(nosan_flags.split(','))
134 args.asan = set_sanitizer('address', args.asan, san, nosan)
135 args.msan = set_sanitizer('memory', args.msan, san, nosan)
136 args.ubsan = set_sanitizer('undefined', args.ubsan, san, nosan)
138 args.sanitize = args.asan or args.msan or args.ubsan
143 def compiler_version(cc, cxx):
145 Determines the compiler and version.
146 Only works for clang and gcc.
148 cc_version_bytes = subprocess.check_output([cc, "--version"])
149 cxx_version_bytes = subprocess.check_output([cxx, "--version"])
150 if cc_version_bytes.startswith(b'clang'):
151 assert(cxx_version_bytes.startswith(b'clang'))
153 if cc_version_bytes.startswith(b'gcc'):
154 assert(cxx_version_bytes.startswith(b'g++'))
156 version_regex = b'([0-9])+\.([0-9])+\.([0-9])+'
157 version_match = re.search(version_regex, cc_version_bytes)
158 version = tuple(int(version_match.group(i)) for i in range(1, 4))
159 return compiler, version
162 def overflow_ubsan_flags(cc, cxx):
163 compiler, version = compiler_version(cc, cxx)
164 if compiler == 'gcc':
165 return ['-fno-sanitize=signed-integer-overflow']
166 if compiler == 'clang' and version >= (5, 0, 0):
167 return ['-fno-sanitize=pointer-overflow']
171 def build_parser(args):
173 Cleans the repository and builds a fuzz target (or all).
174 Many flags default to environment variables (default says $X='y').
175 Options that aren't enabling features default to the correct values for
177 Enable sanitizers with --enable-*san.
178 For regression testing just build.
179 For libFuzzer set LIB_FUZZING_ENGINE and pass --enable-coverage.
180 For AFL set CC and CXX to AFL's compilers and set
181 LIB_FUZZING_ENGINE='libregression.a'.
183 parser = argparse.ArgumentParser(prog=args.pop(0), description=description)
185 '--lib-fuzzing-engine',
186 dest='lib_fuzzing_engine',
188 default=LIB_FUZZING_ENGINE,
189 help=('The fuzzing engine to use e.g. /path/to/libFuzzer.a '
190 "(default: $LIB_FUZZING_ENGINE='{})".format(LIB_FUZZING_ENGINE)))
195 help='Enable coverage instrumentation (-fsanitize-coverage)')
197 '--enable-asan', dest='asan', action='store_true', help='Enable UBSAN')
204 '--enable-ubsan-pointer-overflow',
205 dest='ubsan_pointer_overflow',
207 help='Enable UBSAN pointer overflow check (known failure)')
209 '--enable-msan', dest='msan', action='store_true', help='Enable MSAN')
211 '--enable-msan-track-origins', dest='msan_track_origins',
212 action='store_true', help='Enable MSAN origin tracking')
214 '--msan-extra-cppflags',
215 dest='msan_extra_cppflags',
217 default=MSAN_EXTRA_CPPFLAGS,
218 help="Extra CPPFLAGS for MSAN (default: $MSAN_EXTRA_CPPFLAGS='{}')".
219 format(MSAN_EXTRA_CPPFLAGS))
221 '--msan-extra-cflags',
222 dest='msan_extra_cflags',
224 default=MSAN_EXTRA_CFLAGS,
225 help="Extra CFLAGS for MSAN (default: $MSAN_EXTRA_CFLAGS='{}')".format(
228 '--msan-extra-cxxflags',
229 dest='msan_extra_cxxflags',
231 default=MSAN_EXTRA_CXXFLAGS,
232 help="Extra CXXFLAGS for MSAN (default: $MSAN_EXTRA_CXXFLAGS='{}')".
233 format(MSAN_EXTRA_CXXFLAGS))
235 '--msan-extra-ldflags',
236 dest='msan_extra_ldflags',
238 default=MSAN_EXTRA_LDFLAGS,
239 help="Extra LDFLAGS for MSAN (default: $MSAN_EXTRA_LDFLAGS='{}')".
240 format(MSAN_EXTRA_LDFLAGS))
242 '--enable-sanitize-recover',
243 dest='sanitize_recover',
245 help='Non-fatal sanitizer errors where possible')
251 help='Set ZSTD_DEBUG (default: 1)')
253 '--force-memory-access',
254 dest='memory_access',
257 help='Set MEM_FORCE_MEMORY_ACCESS (default: 0)')
259 '--fuzz-rng-seed-size',
260 dest='fuzz_rng_seed_size',
263 help='Set FUZZ_RNG_SEED_SIZE (default: 4)')
265 '--disable-fuzzing-mode',
267 action='store_false',
268 help='Do not define FUZZING_BUILD_MORE_UNSAFE_FOR_PRODUCTION')
270 '--enable-stateful-fuzzing',
271 dest='stateful_fuzzing',
273 help='Reuse contexts between runs (makes reproduction impossible)')
279 help="CC (default: $CC='{}')".format(CC))
285 help="CXX (default: $CXX='{}')".format(CXX))
291 help="CPPFLAGS (default: $CPPFLAGS='{}')".format(CPPFLAGS))
297 help="CFLAGS (default: $CFLAGS='{}')".format(CFLAGS))
303 help="CXXFLAGS (default: $CXXFLAGS='{}')".format(CXXFLAGS))
309 help="LDFLAGS (default: $LDFLAGS='{}')".format(LDFLAGS))
315 help="Extra Make flags (default: $MFLAGS='{}')".format(MFLAGS))
320 help='Fuzz target(s) to build {{{}}}'.format(', '.join(ALL_TARGETS))
322 args = parser.parse_args(args)
323 args = parse_env_flags(args, ' '.join(
324 [args.cppflags, args.cflags, args.cxxflags, args.ldflags]))
326 # Check option sanitiy
327 if args.msan and (args.asan or args.ubsan):
328 raise RuntimeError('MSAN may not be used with any other sanitizers')
329 if args.msan_track_origins and not args.msan:
330 raise RuntimeError('--enable-msan-track-origins requires MSAN')
331 if args.ubsan_pointer_overflow and not args.ubsan:
332 raise RuntimeError('--enable-ubsan-pointer-overlow requires UBSAN')
333 if args.sanitize_recover and not args.sanitize:
334 raise RuntimeError('--enable-sanitize-recover but no sanitizers used')
341 args = build_parser(args)
342 except Exception as e:
345 # The compilation flags we are setting
346 targets = args.TARGET
349 cppflags = [args.cppflags]
350 cflags = [args.cflags]
351 ldflags = [args.ldflags]
352 cxxflags = [args.cxxflags]
353 mflags = [args.mflags] if args.mflags else []
354 # Flags to be added to both cflags and cxxflags
358 '-DZSTD_DEBUG={}'.format(args.debug),
359 '-DMEM_FORCE_MEMORY_ACCESS={}'.format(args.memory_access),
360 '-DFUZZ_RNG_SEED_SIZE={}'.format(args.fuzz_rng_seed_size),
363 mflags += ['LIB_FUZZING_ENGINE={}'.format(args.lib_fuzzing_engine)]
365 # Set flags for options
368 '-fsanitize-coverage=trace-pc-guard,indirect-calls,trace-cmp'
371 if args.sanitize_recover:
372 recover_flags = ['-fsanitize-recover=all']
374 recover_flags = ['-fno-sanitize-recover=all']
376 common_flags += recover_flags
379 msan_flags = ['-fsanitize=memory']
380 if args.msan_track_origins:
381 msan_flags += ['-fsanitize-memory-track-origins']
382 common_flags += msan_flags
383 # Append extra MSAN flags (it might require special setup)
384 cppflags += [args.msan_extra_cppflags]
385 cflags += [args.msan_extra_cflags]
386 cxxflags += [args.msan_extra_cxxflags]
387 ldflags += [args.msan_extra_ldflags]
390 common_flags += ['-fsanitize=address']
393 ubsan_flags = ['-fsanitize=undefined']
394 if not args.ubsan_pointer_overflow:
395 ubsan_flags += overflow_ubsan_flags(cc, cxx)
396 common_flags += ubsan_flags
398 if args.stateful_fuzzing:
399 cppflags += ['-DSTATEFUL_FUZZING']
401 if args.fuzzing_mode:
402 cppflags += ['-DFUZZING_BUILD_MORE_UNSAFE_FOR_PRODUCTION']
404 if args.lib_fuzzing_engine == 'libregression.a':
405 targets = ['libregression.a'] + targets
407 # Append the common flags
408 cflags += common_flags
409 cxxflags += common_flags
411 # Prepare the flags for Make
412 cc_str = "CC={}".format(cc)
413 cxx_str = "CXX={}".format(cxx)
414 cppflags_str = "CPPFLAGS={}".format(' '.join(cppflags))
415 cflags_str = "CFLAGS={}".format(' '.join(cflags))
416 cxxflags_str = "CXXFLAGS={}".format(' '.join(cxxflags))
417 ldflags_str = "LDFLAGS={}".format(' '.join(ldflags))
420 print('MFLAGS={}'.format(' '.join(mflags)))
429 clean_cmd = ['make', 'clean'] + mflags
430 print(' '.join(clean_cmd))
431 subprocess.check_call(clean_cmd)
441 print(' '.join(build_cmd))
442 subprocess.check_call(build_cmd)
446 def libfuzzer_parser(args):
448 Runs a libfuzzer binary.
449 Passes all extra arguments to libfuzzer.
450 The fuzzer should have been build with LIB_FUZZING_ENGINE pointing to
452 Generates output in the CORPORA directory, puts crashes in the ARTIFACT
453 directory, and takes extra input from the SEED directory.
454 To merge AFL's output pass the SEED as AFL's output directory and pass
457 parser = argparse.ArgumentParser(prog=args.pop(0), description=description)
461 help='Override the default corpora dir (default: {})'.format(
462 abs_join(CORPORA_DIR, 'TARGET')))
466 help='Override the default artifact dir (default: {})'.format(
467 abs_join(CORPORA_DIR, 'TARGET-crash')))
471 help='Override the default seed dir (default: {})'.format(
472 abs_join(CORPORA_DIR, 'TARGET-seed')))
476 help='Fuzz target(s) to build {{{}}}'.format(', '.join(TARGETS)))
477 args, extra = parser.parse_known_args(args)
480 if args.TARGET and args.TARGET not in TARGETS:
481 raise RuntimeError('{} is not a valid target'.format(args.TARGET))
486 def libfuzzer(target, corpora=None, artifact=None, seed=None, extra_args=None):
488 corpora = abs_join(CORPORA_DIR, target)
490 artifact = abs_join(CORPORA_DIR, '{}-crash'.format(target))
492 seed = abs_join(CORPORA_DIR, '{}-seed'.format(target))
493 if extra_args is None:
496 target = abs_join(FUZZ_DIR, target)
498 corpora = [create(corpora)]
499 artifact = create(artifact)
502 corpora += [artifact]
506 cmd = [target, '-artifact_prefix={}/'.format(artifact)]
507 cmd += corpora + extra_args
509 subprocess.check_call(cmd)
512 def libfuzzer_cmd(args):
514 args = libfuzzer_parser(args)
515 except Exception as e:
518 libfuzzer(args.TARGET, args.corpora, args.artifact, args.seed, args.extra)
522 def afl_parser(args):
524 Runs an afl-fuzz job.
525 Passes all extra arguments to afl-fuzz.
526 The fuzzer should have been built with CC/CXX set to the AFL compilers,
527 and with LIB_FUZZING_ENGINE='libregression.a'.
528 Takes input from CORPORA and writes output to OUTPUT.
529 Uses AFL_FUZZ as the binary (set from flag or environment variable).
531 parser = argparse.ArgumentParser(prog=args.pop(0), description=description)
535 help='Override the default corpora dir (default: {})'.format(
536 abs_join(CORPORA_DIR, 'TARGET')))
540 help='Override the default AFL output dir (default: {})'.format(
541 abs_join(CORPORA_DIR, 'TARGET-afl')))
546 help='AFL_FUZZ (default: $AFL_FUZZ={})'.format(AFL_FUZZ))
550 help='Fuzz target(s) to build {{{}}}'.format(', '.join(TARGETS)))
551 args, extra = parser.parse_known_args(args)
554 if args.TARGET and args.TARGET not in TARGETS:
555 raise RuntimeError('{} is not a valid target'.format(args.TARGET))
558 args.corpora = abs_join(CORPORA_DIR, args.TARGET)
560 args.output = abs_join(CORPORA_DIR, '{}-afl'.format(args.TARGET))
567 args = afl_parser(args)
568 except Exception as e:
571 target = abs_join(FUZZ_DIR, args.TARGET)
573 corpora = create(args.corpora)
574 output = create(args.output)
576 cmd = [args.afl_fuzz, '-i', corpora, '-o', output] + args.extra
577 cmd += [target, '@@']
583 def regression(args):
586 Runs one or more regression tests.
587 The fuzzer should have been built with with
588 LIB_FUZZING_ENGINE='libregression.a'.
589 Takes input from CORPORA.
591 args = targets_parser(args, description)
592 except Exception as e:
595 for target in args.TARGET:
596 corpora = create(abs_join(CORPORA_DIR, target))
597 target = abs_join(FUZZ_DIR, target)
598 cmd = [target, corpora]
600 subprocess.check_call(cmd)
604 def gen_parser(args):
606 Generate a seed corpus appropiate for TARGET with data generated with
608 The fuzz inputs are prepended with a seed before the zstd data, so the
609 output of decodecorpus shouldn't be used directly.
610 Generates NUMBER samples prepended with FUZZ_RNG_SEED_SIZE random bytes and
611 puts the output in SEED.
612 DECODECORPUS is the decodecorpus binary, and must already be built.
614 parser = argparse.ArgumentParser(prog=args.pop(0), description=description)
620 help='Number of samples to generate')
625 help='Maximum sample size to generate')
629 help='Override the default seed dir (default: {})'.format(
630 abs_join(CORPORA_DIR, 'TARGET-seed')))
634 default=DECODECORPUS,
635 help="decodecorpus binary (default: $DECODECORPUS='{}')".format(
638 '--fuzz-rng-seed-size',
641 help="FUZZ_RNG_SEED_SIZE used for generate the samples (must match)"
646 help='Fuzz target(s) to build {{{}}}'.format(', '.join(TARGETS)))
647 args, extra = parser.parse_known_args(args)
650 if args.TARGET and args.TARGET not in TARGETS:
651 raise RuntimeError('{} is not a valid target'.format(args.TARGET))
654 args.seed = abs_join(CORPORA_DIR, '{}-seed'.format(args.TARGET))
656 if not os.path.isfile(args.decodecorpus):
657 raise RuntimeError("{} is not a file run 'make -C {} decodecorpus'".
658 format(args.decodecorpus, abs_join(FUZZ_DIR, '..')))
665 args = gen_parser(args)
666 except Exception as e:
670 seed = create(args.seed)
671 with tmpdir() as compressed:
672 with tmpdir() as decompressed:
675 '-n{}'.format(args.number),
676 '-p{}/'.format(compressed),
677 '-o{}'.format(decompressed),
680 if 'block_' in args.TARGET:
683 '--max-block-size-log={}'.format(args.max_size_log)
686 cmd += ['--max-content-size-log={}'.format(args.max_size_log)]
689 subprocess.check_call(cmd)
691 if '_round_trip' in args.TARGET:
692 print('using decompressed data in {}'.format(decompressed))
693 samples = decompressed
694 elif '_decompress' in args.TARGET:
695 print('using compressed data in {}'.format(compressed))
698 # Copy the samples over and prepend the RNG seeds
699 for name in os.listdir(samples):
700 samplename = abs_join(samples, name)
701 outname = abs_join(seed, name)
702 rng_seed = os.urandom(args.fuzz_rng_seed_size)
703 with open(samplename, 'rb') as sample:
704 with open(outname, 'wb') as out:
707 chunk = sample.read(CHUNK_SIZE)
708 while len(chunk) > 0:
710 chunk = sample.read(CHUNK_SIZE)
717 Runs a libfuzzer fuzzer with -merge=1 to build a minimal corpus in
718 TARGET_seed_corpus. All extra args are passed to libfuzzer.
720 args = targets_parser(args, description)
721 except Exception as e:
725 for target in args.TARGET:
726 # Merge the corpus + anything else into the seed_corpus
727 corpus = abs_join(CORPORA_DIR, target)
728 seed_corpus = abs_join(CORPORA_DIR, "{}_seed_corpus".format(target))
729 extra_args = [corpus, "-merge=1"] + args.extra
730 libfuzzer(target, corpora=seed_corpus, extra_args=extra_args)
731 seeds = set(os.listdir(seed_corpus))
732 # Copy all crashes directly into the seed_corpus if not already present
733 crashes = abs_join(CORPORA_DIR, '{}-crash'.format(target))
734 for crash in os.listdir(crashes):
735 if crash not in seeds:
736 shutil.copy(abs_join(crashes, crash), seed_corpus)
743 Zips up the seed corpus.
745 args = targets_parser(args, description)
746 except Exception as e:
750 for target in args.TARGET:
751 # Zip the seed_corpus
752 seed_corpus = abs_join(CORPORA_DIR, "{}_seed_corpus".format(target))
753 seeds = [abs_join(seed_corpus, f) for f in os.listdir(seed_corpus)]
754 zip_file = "{}.zip".format(seed_corpus)
755 cmd = ["zip", "-q", "-j", "-9", zip_file]
756 print(' '.join(cmd + [abs_join(seed_corpus, '*')]))
757 subprocess.check_call(cmd + seeds)
761 print("\n".join(TARGETS))
764 def short_help(args):
766 print("Usage: {} [OPTIONS] COMMAND [ARGS]...\n".format(name))
771 print("\tfuzzing helpers (select a command and pass -h for help)\n")
773 print("\t-h, --help\tPrint this message")
776 print("\tbuild\t\tBuild a fuzzer")
777 print("\tlibfuzzer\tRun a libFuzzer fuzzer")
778 print("\tafl\t\tRun an AFL fuzzer")
779 print("\tregression\tRun a regression test")
780 print("\tgen\t\tGenerate a seed corpus for a fuzzer")
781 print("\tminimize\tMinimize the test corpora")
782 print("\tzip\t\tZip the minimized corpora up")
783 print("\tlist\t\tList the available targets")
791 if args[1] == '-h' or args[1] == '--help' or args[1] == '-H':
794 command = args.pop(1)
795 args[0] = "{} {}".format(args[0], command)
796 if command == "build":
798 if command == "libfuzzer":
799 return libfuzzer_cmd(args)
800 if command == "regression":
801 return regression(args)
806 if command == "minimize":
807 return minimize(args)
810 if command == "list":
811 return list_cmd(args)
813 print("Error: No such command {} (pass -h for help)".format(command))
817 if __name__ == "__main__":