1 1.1 christos #!/usr/bin/env python 2 1.1 christos 3 1.1 christos # ################################################################ 4 1.1 christos # Copyright (c) Meta Platforms, Inc. and affiliates. 5 1.1 christos # All rights reserved. 6 1.1 christos # 7 1.1 christos # This source code is licensed under both the BSD-style license (found in the 8 1.1 christos # LICENSE file in the root directory of this source tree) and the GPLv2 (found 9 1.1 christos # in the COPYING file in the root directory of this source tree). 10 1.1 christos # You may select, at your option, one of the above-listed licenses. 11 1.1 christos # ########################################################################## 12 1.1 christos 13 1.1 christos import argparse 14 1.1 christos import contextlib 15 1.1 christos import os 16 1.1 christos import re 17 1.1 christos import shlex 18 1.1 christos import shutil 19 1.1 christos import subprocess 20 1.1 christos import sys 21 1.1 christos import tempfile 22 1.1 christos 23 1.1 christos 24 1.1 christos def abs_join(a, *p): 25 1.1 christos return os.path.abspath(os.path.join(a, *p)) 26 1.1 christos 27 1.1 christos 28 1.1 christos class InputType(object): 29 1.1 christos RAW_DATA = 1 30 1.1 christos COMPRESSED_DATA = 2 31 1.1 christos DICTIONARY_DATA = 3 32 1.1 christos 33 1.1 christos 34 1.1 christos class FrameType(object): 35 1.1 christos ZSTD = 1 36 1.1 christos BLOCK = 2 37 1.1 christos 38 1.1 christos 39 1.1 christos class TargetInfo(object): 40 1.1 christos def __init__(self, input_type, frame_type=FrameType.ZSTD): 41 1.1 christos self.input_type = input_type 42 1.1 christos self.frame_type = frame_type 43 1.1 christos 44 1.1 christos 45 1.1 christos # Constants 46 1.1 christos FUZZ_DIR = os.path.abspath(os.path.dirname(__file__)) 47 1.1 christos CORPORA_DIR = abs_join(FUZZ_DIR, 'corpora') 48 1.1 christos TARGET_INFO = { 49 1.1 christos 'simple_round_trip': TargetInfo(InputType.RAW_DATA), 50 1.1 christos 'stream_round_trip': TargetInfo(InputType.RAW_DATA), 51 1.1 christos 'block_round_trip': TargetInfo(InputType.RAW_DATA, FrameType.BLOCK), 52 1.1 christos 'simple_decompress': TargetInfo(InputType.COMPRESSED_DATA), 53 1.1 christos 'stream_decompress': TargetInfo(InputType.COMPRESSED_DATA), 54 1.1 christos 'block_decompress': TargetInfo(InputType.COMPRESSED_DATA, FrameType.BLOCK), 55 1.1 christos 'dictionary_round_trip': TargetInfo(InputType.RAW_DATA), 56 1.1 christos 'dictionary_decompress': TargetInfo(InputType.COMPRESSED_DATA), 57 1.1 christos 'zstd_frame_info': TargetInfo(InputType.COMPRESSED_DATA), 58 1.1 christos 'simple_compress': TargetInfo(InputType.RAW_DATA), 59 1.1 christos 'dictionary_loader': TargetInfo(InputType.DICTIONARY_DATA), 60 1.1 christos 'raw_dictionary_round_trip': TargetInfo(InputType.RAW_DATA), 61 1.1 christos 'dictionary_stream_round_trip': TargetInfo(InputType.RAW_DATA), 62 1.1 christos 'decompress_dstSize_tooSmall': TargetInfo(InputType.RAW_DATA), 63 1.1 christos 'fse_read_ncount': TargetInfo(InputType.RAW_DATA), 64 1.1 christos 'sequence_compression_api': TargetInfo(InputType.RAW_DATA), 65 1.1 christos 'seekable_roundtrip': TargetInfo(InputType.RAW_DATA), 66 1.1 christos 'huf_round_trip': TargetInfo(InputType.RAW_DATA), 67 1.1 christos 'huf_decompress': TargetInfo(InputType.RAW_DATA), 68 1.1 christos 'decompress_cross_format': TargetInfo(InputType.RAW_DATA), 69 1.1 christos 'generate_sequences': TargetInfo(InputType.RAW_DATA), 70 1.1 christos } 71 1.1 christos TARGETS = list(TARGET_INFO.keys()) 72 1.1 christos ALL_TARGETS = TARGETS + ['all'] 73 1.1 christos FUZZ_RNG_SEED_SIZE = 4 74 1.1 christos 75 1.1 christos # Standard environment variables 76 1.1 christos CC = os.environ.get('CC', 'cc') 77 1.1 christos CXX = os.environ.get('CXX', 'c++') 78 1.1 christos CPPFLAGS = os.environ.get('CPPFLAGS', '') 79 1.1 christos CFLAGS = os.environ.get('CFLAGS', '-O3') 80 1.1 christos CXXFLAGS = os.environ.get('CXXFLAGS', CFLAGS) 81 1.1 christos LDFLAGS = os.environ.get('LDFLAGS', '') 82 1.1 christos MFLAGS = os.environ.get('MFLAGS', '-j') 83 1.1 christos THIRD_PARTY_SEQ_PROD_OBJ = os.environ.get('THIRD_PARTY_SEQ_PROD_OBJ', '') 84 1.1 christos 85 1.1 christos # Fuzzing environment variables 86 1.1 christos LIB_FUZZING_ENGINE = os.environ.get('LIB_FUZZING_ENGINE', 'libregression.a') 87 1.1 christos AFL_FUZZ = os.environ.get('AFL_FUZZ', 'afl-fuzz') 88 1.1 christos DECODECORPUS = os.environ.get('DECODECORPUS', 89 1.1 christos abs_join(FUZZ_DIR, '..', 'decodecorpus')) 90 1.1 christos ZSTD = os.environ.get('ZSTD', abs_join(FUZZ_DIR, '..', '..', 'zstd')) 91 1.1 christos 92 1.1 christos # Sanitizer environment variables 93 1.1 christos MSAN_EXTRA_CPPFLAGS = os.environ.get('MSAN_EXTRA_CPPFLAGS', '') 94 1.1 christos MSAN_EXTRA_CFLAGS = os.environ.get('MSAN_EXTRA_CFLAGS', '') 95 1.1 christos MSAN_EXTRA_CXXFLAGS = os.environ.get('MSAN_EXTRA_CXXFLAGS', '') 96 1.1 christos MSAN_EXTRA_LDFLAGS = os.environ.get('MSAN_EXTRA_LDFLAGS', '') 97 1.1 christos 98 1.1 christos 99 1.1 christos def create(r): 100 1.1 christos d = os.path.abspath(r) 101 1.1 christos if not os.path.isdir(d): 102 1.1 christos os.makedirs(d) 103 1.1 christos return d 104 1.1 christos 105 1.1 christos 106 1.1 christos def check(r): 107 1.1 christos d = os.path.abspath(r) 108 1.1 christos if not os.path.isdir(d): 109 1.1 christos return None 110 1.1 christos return d 111 1.1 christos 112 1.1 christos 113 1.1 christos @contextlib.contextmanager 114 1.1 christos def tmpdir(): 115 1.1 christos dirpath = tempfile.mkdtemp() 116 1.1 christos try: 117 1.1 christos yield dirpath 118 1.1 christos finally: 119 1.1 christos shutil.rmtree(dirpath, ignore_errors=True) 120 1.1 christos 121 1.1 christos 122 1.1 christos def parse_targets(in_targets): 123 1.1 christos targets = set() 124 1.1 christos for target in in_targets: 125 1.1 christos if not target: 126 1.1 christos continue 127 1.1 christos if target == 'all': 128 1.1 christos targets = targets.union(TARGETS) 129 1.1 christos elif target in TARGETS: 130 1.1 christos targets.add(target) 131 1.1 christos else: 132 1.1 christos raise RuntimeError('{} is not a valid target'.format(target)) 133 1.1 christos return list(targets) 134 1.1 christos 135 1.1 christos 136 1.1 christos def targets_parser(args, description): 137 1.1 christos parser = argparse.ArgumentParser(prog=args.pop(0), description=description) 138 1.1 christos parser.add_argument( 139 1.1 christos 'TARGET', 140 1.1 christos nargs='*', 141 1.1 christos type=str, 142 1.1 christos help='Fuzz target(s) to build {{{}}}'.format(', '.join(ALL_TARGETS))) 143 1.1 christos args, extra = parser.parse_known_args(args) 144 1.1 christos args.extra = extra 145 1.1 christos 146 1.1 christos args.TARGET = parse_targets(args.TARGET) 147 1.1 christos 148 1.1 christos return args 149 1.1 christos 150 1.1 christos 151 1.1 christos def parse_env_flags(args, flags): 152 1.1 christos """ 153 1.1 christos Look for flags set by environment variables. 154 1.1 christos """ 155 1.1 christos san_flags = ','.join(re.findall('-fsanitize=((?:[a-z]+,?)+)', flags)) 156 1.1 christos nosan_flags = ','.join(re.findall('-fno-sanitize=((?:[a-z]+,?)+)', flags)) 157 1.1 christos 158 1.1 christos def set_sanitizer(sanitizer, default, san, nosan): 159 1.1 christos if sanitizer in san and sanitizer in nosan: 160 1.1 christos raise RuntimeError('-fno-sanitize={s} and -fsanitize={s} passed'. 161 1.1 christos format(s=sanitizer)) 162 1.1 christos if sanitizer in san: 163 1.1 christos return True 164 1.1 christos if sanitizer in nosan: 165 1.1 christos return False 166 1.1 christos return default 167 1.1 christos 168 1.1 christos san = set(san_flags.split(',')) 169 1.1 christos nosan = set(nosan_flags.split(',')) 170 1.1 christos 171 1.1 christos args.asan = set_sanitizer('address', args.asan, san, nosan) 172 1.1 christos args.msan = set_sanitizer('memory', args.msan, san, nosan) 173 1.1 christos args.ubsan = set_sanitizer('undefined', args.ubsan, san, nosan) 174 1.1 christos 175 1.1 christos args.sanitize = args.asan or args.msan or args.ubsan 176 1.1 christos 177 1.1 christos return args 178 1.1 christos 179 1.1 christos 180 1.1 christos def compiler_version(cc, cxx): 181 1.1 christos """ 182 1.1 christos Determines the compiler and version. 183 1.1 christos Only works for clang and gcc. 184 1.1 christos """ 185 1.1 christos cc_version_bytes = subprocess.check_output([cc, "--version"]) 186 1.1 christos cxx_version_bytes = subprocess.check_output([cxx, "--version"]) 187 1.1 christos compiler = None 188 1.1 christos version = None 189 1.1 christos print("{} --version:\n{}".format(cc, cc_version_bytes.decode('ascii'))) 190 1.1 christos if b'clang' in cc_version_bytes: 191 1.1 christos assert(b'clang' in cxx_version_bytes) 192 1.1 christos compiler = 'clang' 193 1.1 christos elif b'gcc' in cc_version_bytes or b'GCC' in cc_version_bytes: 194 1.1 christos assert(b'gcc' in cxx_version_bytes or b'g++' in cxx_version_bytes) 195 1.1 christos compiler = 'gcc' 196 1.1 christos if compiler is not None: 197 1.1 christos version_regex = b'([0-9]+)\.([0-9]+)\.([0-9]+)' 198 1.1 christos version_match = re.search(version_regex, cc_version_bytes) 199 1.1 christos version = tuple(int(version_match.group(i)) for i in range(1, 4)) 200 1.1 christos return compiler, version 201 1.1 christos 202 1.1 christos 203 1.1 christos def overflow_ubsan_flags(cc, cxx): 204 1.1 christos compiler, version = compiler_version(cc, cxx) 205 1.1 christos if compiler == 'gcc' and version < (8, 0, 0): 206 1.1 christos return ['-fno-sanitize=signed-integer-overflow'] 207 1.1 christos if compiler == 'gcc' or (compiler == 'clang' and version >= (5, 0, 0)): 208 1.1 christos return ['-fno-sanitize=pointer-overflow'] 209 1.1 christos return [] 210 1.1 christos 211 1.1 christos 212 1.1 christos def build_parser(args): 213 1.1 christos description = """ 214 1.1 christos Cleans the repository and builds a fuzz target (or all). 215 1.1 christos Many flags default to environment variables (default says $X='y'). 216 1.1 christos Options that aren't enabling features default to the correct values for 217 1.1 christos zstd. 218 1.1 christos Enable sanitizers with --enable-*san. 219 1.1 christos For regression testing just build. 220 1.1 christos For libFuzzer set LIB_FUZZING_ENGINE and pass --enable-coverage. 221 1.1 christos For AFL set CC and CXX to AFL's compilers and set 222 1.1 christos LIB_FUZZING_ENGINE='libregression.a'. 223 1.1 christos """ 224 1.1 christos parser = argparse.ArgumentParser(prog=args.pop(0), description=description) 225 1.1 christos parser.add_argument( 226 1.1 christos '--lib-fuzzing-engine', 227 1.1 christos dest='lib_fuzzing_engine', 228 1.1 christos type=str, 229 1.1 christos default=LIB_FUZZING_ENGINE, 230 1.1 christos help=('The fuzzing engine to use e.g. /path/to/libFuzzer.a ' 231 1.1 christos "(default: $LIB_FUZZING_ENGINE='{})".format(LIB_FUZZING_ENGINE))) 232 1.1 christos 233 1.1 christos fuzz_group = parser.add_mutually_exclusive_group() 234 1.1 christos fuzz_group.add_argument( 235 1.1 christos '--enable-coverage', 236 1.1 christos dest='coverage', 237 1.1 christos action='store_true', 238 1.1 christos help='Enable coverage instrumentation (-fsanitize-coverage)') 239 1.1 christos fuzz_group.add_argument( 240 1.1 christos '--enable-fuzzer', 241 1.1 christos dest='fuzzer', 242 1.1 christos action='store_true', 243 1.1 christos help=('Enable clang fuzzer (-fsanitize=fuzzer). When enabled ' 244 1.1 christos 'LIB_FUZZING_ENGINE is ignored') 245 1.1 christos ) 246 1.1 christos 247 1.1 christos parser.add_argument( 248 1.1 christos '--enable-asan', dest='asan', action='store_true', help='Enable UBSAN') 249 1.1 christos parser.add_argument( 250 1.1 christos '--enable-ubsan', 251 1.1 christos dest='ubsan', 252 1.1 christos action='store_true', 253 1.1 christos help='Enable UBSAN') 254 1.1 christos parser.add_argument( 255 1.1 christos '--disable-ubsan-pointer-overflow', 256 1.1 christos dest='ubsan_pointer_overflow', 257 1.1 christos action='store_false', 258 1.1 christos help='Disable UBSAN pointer overflow check (known failure)') 259 1.1 christos parser.add_argument( 260 1.1 christos '--enable-msan', dest='msan', action='store_true', help='Enable MSAN') 261 1.1 christos parser.add_argument( 262 1.1 christos '--enable-msan-track-origins', dest='msan_track_origins', 263 1.1 christos action='store_true', help='Enable MSAN origin tracking') 264 1.1 christos parser.add_argument( 265 1.1 christos '--msan-extra-cppflags', 266 1.1 christos dest='msan_extra_cppflags', 267 1.1 christos type=str, 268 1.1 christos default=MSAN_EXTRA_CPPFLAGS, 269 1.1 christos help="Extra CPPFLAGS for MSAN (default: $MSAN_EXTRA_CPPFLAGS='{}')". 270 1.1 christos format(MSAN_EXTRA_CPPFLAGS)) 271 1.1 christos parser.add_argument( 272 1.1 christos '--msan-extra-cflags', 273 1.1 christos dest='msan_extra_cflags', 274 1.1 christos type=str, 275 1.1 christos default=MSAN_EXTRA_CFLAGS, 276 1.1 christos help="Extra CFLAGS for MSAN (default: $MSAN_EXTRA_CFLAGS='{}')".format( 277 1.1 christos MSAN_EXTRA_CFLAGS)) 278 1.1 christos parser.add_argument( 279 1.1 christos '--msan-extra-cxxflags', 280 1.1 christos dest='msan_extra_cxxflags', 281 1.1 christos type=str, 282 1.1 christos default=MSAN_EXTRA_CXXFLAGS, 283 1.1 christos help="Extra CXXFLAGS for MSAN (default: $MSAN_EXTRA_CXXFLAGS='{}')". 284 1.1 christos format(MSAN_EXTRA_CXXFLAGS)) 285 1.1 christos parser.add_argument( 286 1.1 christos '--msan-extra-ldflags', 287 1.1 christos dest='msan_extra_ldflags', 288 1.1 christos type=str, 289 1.1 christos default=MSAN_EXTRA_LDFLAGS, 290 1.1 christos help="Extra LDFLAGS for MSAN (default: $MSAN_EXTRA_LDFLAGS='{}')". 291 1.1 christos format(MSAN_EXTRA_LDFLAGS)) 292 1.1 christos parser.add_argument( 293 1.1 christos '--enable-sanitize-recover', 294 1.1 christos dest='sanitize_recover', 295 1.1 christos action='store_true', 296 1.1 christos help='Non-fatal sanitizer errors where possible') 297 1.1 christos parser.add_argument( 298 1.1 christos '--debug', 299 1.1 christos dest='debug', 300 1.1 christos type=int, 301 1.1 christos default=1, 302 1.1 christos help='Set DEBUGLEVEL (default: 1)') 303 1.1 christos parser.add_argument( 304 1.1 christos '--force-memory-access', 305 1.1 christos dest='memory_access', 306 1.1 christos type=int, 307 1.1 christos default=0, 308 1.1 christos help='Set MEM_FORCE_MEMORY_ACCESS (default: 0)') 309 1.1 christos parser.add_argument( 310 1.1 christos '--fuzz-rng-seed-size', 311 1.1 christos dest='fuzz_rng_seed_size', 312 1.1 christos type=int, 313 1.1 christos default=4, 314 1.1 christos help='Set FUZZ_RNG_SEED_SIZE (default: 4)') 315 1.1 christos parser.add_argument( 316 1.1 christos '--disable-fuzzing-mode', 317 1.1 christos dest='fuzzing_mode', 318 1.1 christos action='store_false', 319 1.1 christos help='Do not define FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION') 320 1.1 christos parser.add_argument( 321 1.1 christos '--enable-stateful-fuzzing', 322 1.1 christos dest='stateful_fuzzing', 323 1.1 christos action='store_true', 324 1.1 christos help='Reuse contexts between runs (makes reproduction impossible)') 325 1.1 christos parser.add_argument( 326 1.1 christos '--custom-seq-prod', 327 1.1 christos dest='third_party_seq_prod_obj', 328 1.1 christos type=str, 329 1.1 christos default=THIRD_PARTY_SEQ_PROD_OBJ, 330 1.1 christos help='Path to an object file with symbols for fuzzing your sequence producer plugin.') 331 1.1 christos parser.add_argument( 332 1.1 christos '--cc', 333 1.1 christos dest='cc', 334 1.1 christos type=str, 335 1.1 christos default=CC, 336 1.1 christos help="CC (default: $CC='{}')".format(CC)) 337 1.1 christos parser.add_argument( 338 1.1 christos '--cxx', 339 1.1 christos dest='cxx', 340 1.1 christos type=str, 341 1.1 christos default=CXX, 342 1.1 christos help="CXX (default: $CXX='{}')".format(CXX)) 343 1.1 christos parser.add_argument( 344 1.1 christos '--cppflags', 345 1.1 christos dest='cppflags', 346 1.1 christos type=str, 347 1.1 christos default=CPPFLAGS, 348 1.1 christos help="CPPFLAGS (default: $CPPFLAGS='{}')".format(CPPFLAGS)) 349 1.1 christos parser.add_argument( 350 1.1 christos '--cflags', 351 1.1 christos dest='cflags', 352 1.1 christos type=str, 353 1.1 christos default=CFLAGS, 354 1.1 christos help="CFLAGS (default: $CFLAGS='{}')".format(CFLAGS)) 355 1.1 christos parser.add_argument( 356 1.1 christos '--cxxflags', 357 1.1 christos dest='cxxflags', 358 1.1 christos type=str, 359 1.1 christos default=CXXFLAGS, 360 1.1 christos help="CXXFLAGS (default: $CXXFLAGS='{}')".format(CXXFLAGS)) 361 1.1 christos parser.add_argument( 362 1.1 christos '--ldflags', 363 1.1 christos dest='ldflags', 364 1.1 christos type=str, 365 1.1 christos default=LDFLAGS, 366 1.1 christos help="LDFLAGS (default: $LDFLAGS='{}')".format(LDFLAGS)) 367 1.1 christos parser.add_argument( 368 1.1 christos '--mflags', 369 1.1 christos dest='mflags', 370 1.1 christos type=str, 371 1.1 christos default=MFLAGS, 372 1.1 christos help="Extra Make flags (default: $MFLAGS='{}')".format(MFLAGS)) 373 1.1 christos parser.add_argument( 374 1.1 christos 'TARGET', 375 1.1 christos nargs='*', 376 1.1 christos type=str, 377 1.1 christos help='Fuzz target(s) to build {{{}}}'.format(', '.join(ALL_TARGETS)) 378 1.1 christos ) 379 1.1 christos args = parser.parse_args(args) 380 1.1 christos args = parse_env_flags(args, ' '.join( 381 1.1 christos [args.cppflags, args.cflags, args.cxxflags, args.ldflags])) 382 1.1 christos 383 1.1 christos # Check option sanity 384 1.1 christos if args.msan and (args.asan or args.ubsan): 385 1.1 christos raise RuntimeError('MSAN may not be used with any other sanitizers') 386 1.1 christos if args.msan_track_origins and not args.msan: 387 1.1 christos raise RuntimeError('--enable-msan-track-origins requires MSAN') 388 1.1 christos if args.sanitize_recover and not args.sanitize: 389 1.1 christos raise RuntimeError('--enable-sanitize-recover but no sanitizers used') 390 1.1 christos 391 1.1 christos return args 392 1.1 christos 393 1.1 christos 394 1.1 christos def build(args): 395 1.1 christos try: 396 1.1 christos args = build_parser(args) 397 1.1 christos except Exception as e: 398 1.1 christos print(e) 399 1.1 christos return 1 400 1.1 christos # The compilation flags we are setting 401 1.1 christos targets = args.TARGET 402 1.1 christos cc = args.cc 403 1.1 christos cxx = args.cxx 404 1.1 christos cppflags = shlex.split(args.cppflags) 405 1.1 christos cflags = shlex.split(args.cflags) 406 1.1 christos ldflags = shlex.split(args.ldflags) 407 1.1 christos cxxflags = shlex.split(args.cxxflags) 408 1.1 christos mflags = shlex.split(args.mflags) 409 1.1 christos # Flags to be added to both cflags and cxxflags 410 1.1 christos common_flags = [ 411 1.1 christos '-Werror', 412 1.1 christos '-Wno-error=declaration-after-statement', 413 1.1 christos '-Wno-error=c++-compat', 414 1.1 christos '-Wno-error=deprecated' # C files are sometimes compiled with CXX 415 1.1 christos ] 416 1.1 christos 417 1.1 christos cppflags += [ 418 1.1 christos '-DDEBUGLEVEL={}'.format(args.debug), 419 1.1 christos '-DMEM_FORCE_MEMORY_ACCESS={}'.format(args.memory_access), 420 1.1 christos '-DFUZZ_RNG_SEED_SIZE={}'.format(args.fuzz_rng_seed_size), 421 1.1 christos ] 422 1.1 christos 423 1.1 christos # Set flags for options 424 1.1 christos assert not (args.fuzzer and args.coverage) 425 1.1 christos if args.coverage: 426 1.1 christos common_flags += [ 427 1.1 christos '-fsanitize-coverage=trace-pc-guard,indirect-calls,trace-cmp' 428 1.1 christos ] 429 1.1 christos if args.fuzzer: 430 1.1 christos common_flags += ['-fsanitize=fuzzer'] 431 1.1 christos args.lib_fuzzing_engine = '' 432 1.1 christos 433 1.1 christos mflags += ['LIB_FUZZING_ENGINE={}'.format(args.lib_fuzzing_engine)] 434 1.1 christos 435 1.1 christos if args.sanitize_recover: 436 1.1 christos recover_flags = ['-fsanitize-recover=all'] 437 1.1 christos else: 438 1.1 christos recover_flags = ['-fno-sanitize-recover=all'] 439 1.1 christos if args.sanitize: 440 1.1 christos common_flags += recover_flags 441 1.1 christos 442 1.1 christos if args.msan: 443 1.1 christos msan_flags = ['-fsanitize=memory'] 444 1.1 christos if args.msan_track_origins: 445 1.1 christos msan_flags += ['-fsanitize-memory-track-origins'] 446 1.1 christos common_flags += msan_flags 447 1.1 christos # Append extra MSAN flags (it might require special setup) 448 1.1 christos cppflags += [args.msan_extra_cppflags] 449 1.1 christos cflags += [args.msan_extra_cflags] 450 1.1 christos cxxflags += [args.msan_extra_cxxflags] 451 1.1 christos ldflags += [args.msan_extra_ldflags] 452 1.1 christos 453 1.1 christos if args.asan: 454 1.1 christos common_flags += ['-fsanitize=address'] 455 1.1 christos 456 1.1 christos if args.ubsan: 457 1.1 christos ubsan_flags = ['-fsanitize=undefined'] 458 1.1 christos if not args.ubsan_pointer_overflow: 459 1.1 christos ubsan_flags += overflow_ubsan_flags(cc, cxx) 460 1.1 christos common_flags += ubsan_flags 461 1.1 christos 462 1.1 christos if args.stateful_fuzzing: 463 1.1 christos cppflags += ['-DSTATEFUL_FUZZING'] 464 1.1 christos 465 1.1 christos if args.third_party_seq_prod_obj: 466 1.1 christos cppflags += ['-DFUZZ_THIRD_PARTY_SEQ_PROD'] 467 1.1 christos mflags += ['THIRD_PARTY_SEQ_PROD_OBJ={}'.format(args.third_party_seq_prod_obj)] 468 1.1 christos 469 1.1 christos if args.fuzzing_mode: 470 1.1 christos cppflags += ['-DFUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION'] 471 1.1 christos 472 1.1 christos if args.lib_fuzzing_engine == 'libregression.a': 473 1.1 christos targets = ['libregression.a'] + targets 474 1.1 christos 475 1.1 christos # Append the common flags 476 1.1 christos cflags += common_flags 477 1.1 christos cxxflags += common_flags 478 1.1 christos 479 1.1 christos # Prepare the flags for Make 480 1.1 christos cc_str = "CC={}".format(cc) 481 1.1 christos cxx_str = "CXX={}".format(cxx) 482 1.1 christos cppflags_str = "CPPFLAGS={}".format(' '.join(cppflags)) 483 1.1 christos cflags_str = "CFLAGS={}".format(' '.join(cflags)) 484 1.1 christos cxxflags_str = "CXXFLAGS={}".format(' '.join(cxxflags)) 485 1.1 christos ldflags_str = "LDFLAGS={}".format(' '.join(ldflags)) 486 1.1 christos 487 1.1 christos # Print the flags 488 1.1 christos print('MFLAGS={}'.format(' '.join(mflags))) 489 1.1 christos print(cc_str) 490 1.1 christos print(cxx_str) 491 1.1 christos print(cppflags_str) 492 1.1 christos print(cflags_str) 493 1.1 christos print(cxxflags_str) 494 1.1 christos print(ldflags_str) 495 1.1 christos 496 1.1 christos # Clean and build 497 1.1 christos clean_cmd = ['make', 'clean'] + mflags 498 1.1 christos print(' '.join(clean_cmd)) 499 1.1 christos subprocess.check_call(clean_cmd) 500 1.1 christos build_cmd = [ 501 1.1 christos 'make', 502 1.1 christos '-j', 503 1.1 christos cc_str, 504 1.1 christos cxx_str, 505 1.1 christos cppflags_str, 506 1.1 christos cflags_str, 507 1.1 christos cxxflags_str, 508 1.1 christos ldflags_str, 509 1.1 christos ] + mflags + targets 510 1.1 christos print(' '.join(build_cmd)) 511 1.1 christos subprocess.check_call(build_cmd) 512 1.1 christos return 0 513 1.1 christos 514 1.1 christos 515 1.1 christos def libfuzzer_parser(args): 516 1.1 christos description = """ 517 1.1 christos Runs a libfuzzer binary. 518 1.1 christos Passes all extra arguments to libfuzzer. 519 1.1 christos The fuzzer should have been build with LIB_FUZZING_ENGINE pointing to 520 1.1 christos libFuzzer.a. 521 1.1 christos Generates output in the CORPORA directory, puts crashes in the ARTIFACT 522 1.1 christos directory, and takes extra input from the SEED directory. 523 1.1 christos To merge AFL's output pass the SEED as AFL's output directory and pass 524 1.1 christos '-merge=1'. 525 1.1 christos """ 526 1.1 christos parser = argparse.ArgumentParser(prog=args.pop(0), description=description) 527 1.1 christos parser.add_argument( 528 1.1 christos '--corpora', 529 1.1 christos type=str, 530 1.1 christos help='Override the default corpora dir (default: {})'.format( 531 1.1 christos abs_join(CORPORA_DIR, 'TARGET'))) 532 1.1 christos parser.add_argument( 533 1.1 christos '--artifact', 534 1.1 christos type=str, 535 1.1 christos help='Override the default artifact dir (default: {})'.format( 536 1.1 christos abs_join(CORPORA_DIR, 'TARGET-crash'))) 537 1.1 christos parser.add_argument( 538 1.1 christos '--seed', 539 1.1 christos type=str, 540 1.1 christos help='Override the default seed dir (default: {})'.format( 541 1.1 christos abs_join(CORPORA_DIR, 'TARGET-seed'))) 542 1.1 christos parser.add_argument( 543 1.1 christos 'TARGET', 544 1.1 christos type=str, 545 1.1 christos help='Fuzz target(s) to build {{{}}}'.format(', '.join(TARGETS))) 546 1.1 christos args, extra = parser.parse_known_args(args) 547 1.1 christos args.extra = extra 548 1.1 christos 549 1.1 christos if args.TARGET and args.TARGET not in TARGETS: 550 1.1 christos raise RuntimeError('{} is not a valid target'.format(args.TARGET)) 551 1.1 christos 552 1.1 christos return args 553 1.1 christos 554 1.1 christos 555 1.1 christos def libfuzzer(target, corpora=None, artifact=None, seed=None, extra_args=None): 556 1.1 christos if corpora is None: 557 1.1 christos corpora = abs_join(CORPORA_DIR, target) 558 1.1 christos if artifact is None: 559 1.1 christos artifact = abs_join(CORPORA_DIR, '{}-crash'.format(target)) 560 1.1 christos if seed is None: 561 1.1 christos seed = abs_join(CORPORA_DIR, '{}-seed'.format(target)) 562 1.1 christos if extra_args is None: 563 1.1 christos extra_args = [] 564 1.1 christos 565 1.1 christos target = abs_join(FUZZ_DIR, target) 566 1.1 christos 567 1.1 christos corpora = [create(corpora)] 568 1.1 christos artifact = create(artifact) 569 1.1 christos seed = check(seed) 570 1.1 christos 571 1.1 christos corpora += [artifact] 572 1.1 christos if seed is not None: 573 1.1 christos corpora += [seed] 574 1.1 christos 575 1.1 christos cmd = [target, '-artifact_prefix={}/'.format(artifact)] 576 1.1 christos cmd += corpora + extra_args 577 1.1 christos print(' '.join(cmd)) 578 1.1 christos subprocess.check_call(cmd) 579 1.1 christos 580 1.1 christos 581 1.1 christos def libfuzzer_cmd(args): 582 1.1 christos try: 583 1.1 christos args = libfuzzer_parser(args) 584 1.1 christos except Exception as e: 585 1.1 christos print(e) 586 1.1 christos return 1 587 1.1 christos libfuzzer(args.TARGET, args.corpora, args.artifact, args.seed, args.extra) 588 1.1 christos return 0 589 1.1 christos 590 1.1 christos 591 1.1 christos def afl_parser(args): 592 1.1 christos description = """ 593 1.1 christos Runs an afl-fuzz job. 594 1.1 christos Passes all extra arguments to afl-fuzz. 595 1.1 christos The fuzzer should have been built with CC/CXX set to the AFL compilers, 596 1.1 christos and with LIB_FUZZING_ENGINE='libregression.a'. 597 1.1 christos Takes input from CORPORA and writes output to OUTPUT. 598 1.1 christos Uses AFL_FUZZ as the binary (set from flag or environment variable). 599 1.1 christos """ 600 1.1 christos parser = argparse.ArgumentParser(prog=args.pop(0), description=description) 601 1.1 christos parser.add_argument( 602 1.1 christos '--corpora', 603 1.1 christos type=str, 604 1.1 christos help='Override the default corpora dir (default: {})'.format( 605 1.1 christos abs_join(CORPORA_DIR, 'TARGET'))) 606 1.1 christos parser.add_argument( 607 1.1 christos '--output', 608 1.1 christos type=str, 609 1.1 christos help='Override the default AFL output dir (default: {})'.format( 610 1.1 christos abs_join(CORPORA_DIR, 'TARGET-afl'))) 611 1.1 christos parser.add_argument( 612 1.1 christos '--afl-fuzz', 613 1.1 christos type=str, 614 1.1 christos default=AFL_FUZZ, 615 1.1 christos help='AFL_FUZZ (default: $AFL_FUZZ={})'.format(AFL_FUZZ)) 616 1.1 christos parser.add_argument( 617 1.1 christos 'TARGET', 618 1.1 christos type=str, 619 1.1 christos help='Fuzz target(s) to build {{{}}}'.format(', '.join(TARGETS))) 620 1.1 christos args, extra = parser.parse_known_args(args) 621 1.1 christos args.extra = extra 622 1.1 christos 623 1.1 christos if args.TARGET and args.TARGET not in TARGETS: 624 1.1 christos raise RuntimeError('{} is not a valid target'.format(args.TARGET)) 625 1.1 christos 626 1.1 christos if not args.corpora: 627 1.1 christos args.corpora = abs_join(CORPORA_DIR, args.TARGET) 628 1.1 christos if not args.output: 629 1.1 christos args.output = abs_join(CORPORA_DIR, '{}-afl'.format(args.TARGET)) 630 1.1 christos 631 1.1 christos return args 632 1.1 christos 633 1.1 christos 634 1.1 christos def afl(args): 635 1.1 christos try: 636 1.1 christos args = afl_parser(args) 637 1.1 christos except Exception as e: 638 1.1 christos print(e) 639 1.1 christos return 1 640 1.1 christos target = abs_join(FUZZ_DIR, args.TARGET) 641 1.1 christos 642 1.1 christos corpora = create(args.corpora) 643 1.1 christos output = create(args.output) 644 1.1 christos 645 1.1 christos cmd = [args.afl_fuzz, '-i', corpora, '-o', output] + args.extra 646 1.1 christos cmd += [target, '@@'] 647 1.1 christos print(' '.join(cmd)) 648 1.1 christos subprocess.call(cmd) 649 1.1 christos return 0 650 1.1 christos 651 1.1 christos 652 1.1 christos def regression(args): 653 1.1 christos try: 654 1.1 christos description = """ 655 1.1 christos Runs one or more regression tests. 656 1.1 christos The fuzzer should have been built with 657 1.1 christos LIB_FUZZING_ENGINE='libregression.a'. 658 1.1 christos Takes input from CORPORA. 659 1.1 christos """ 660 1.1 christos args = targets_parser(args, description) 661 1.1 christos except Exception as e: 662 1.1 christos print(e) 663 1.1 christos return 1 664 1.1 christos for target in args.TARGET: 665 1.1 christos corpora = create(abs_join(CORPORA_DIR, target)) 666 1.1 christos target = abs_join(FUZZ_DIR, target) 667 1.1 christos cmd = [target, corpora] 668 1.1 christos print(' '.join(cmd)) 669 1.1 christos subprocess.check_call(cmd) 670 1.1 christos return 0 671 1.1 christos 672 1.1 christos 673 1.1 christos def gen_parser(args): 674 1.1 christos description = """ 675 1.1 christos Generate a seed corpus appropriate for TARGET with data generated with 676 1.1 christos decodecorpus. 677 1.1 christos The fuzz inputs are prepended with a seed before the zstd data, so the 678 1.1 christos output of decodecorpus shouldn't be used directly. 679 1.1 christos Generates NUMBER samples prepended with FUZZ_RNG_SEED_SIZE random bytes and 680 1.1 christos puts the output in SEED. 681 1.1 christos DECODECORPUS is the decodecorpus binary, and must already be built. 682 1.1 christos """ 683 1.1 christos parser = argparse.ArgumentParser(prog=args.pop(0), description=description) 684 1.1 christos parser.add_argument( 685 1.1 christos '--number', 686 1.1 christos '-n', 687 1.1 christos type=int, 688 1.1 christos default=100, 689 1.1 christos help='Number of samples to generate') 690 1.1 christos parser.add_argument( 691 1.1 christos '--max-size-log', 692 1.1 christos type=int, 693 1.1 christos default=18, 694 1.1 christos help='Maximum sample size to generate') 695 1.1 christos parser.add_argument( 696 1.1 christos '--seed', 697 1.1 christos type=str, 698 1.1 christos help='Override the default seed dir (default: {})'.format( 699 1.1 christos abs_join(CORPORA_DIR, 'TARGET-seed'))) 700 1.1 christos parser.add_argument( 701 1.1 christos '--decodecorpus', 702 1.1 christos type=str, 703 1.1 christos default=DECODECORPUS, 704 1.1 christos help="decodecorpus binary (default: $DECODECORPUS='{}')".format( 705 1.1 christos DECODECORPUS)) 706 1.1 christos parser.add_argument( 707 1.1 christos '--zstd', 708 1.1 christos type=str, 709 1.1 christos default=ZSTD, 710 1.1 christos help="zstd binary (default: $ZSTD='{}')".format(ZSTD)) 711 1.1 christos parser.add_argument( 712 1.1 christos '--fuzz-rng-seed-size', 713 1.1 christos type=int, 714 1.1 christos default=4, 715 1.1 christos help="FUZZ_RNG_SEED_SIZE used for generate the samples (must match)" 716 1.1 christos ) 717 1.1 christos parser.add_argument( 718 1.1 christos 'TARGET', 719 1.1 christos type=str, 720 1.1 christos help='Fuzz target(s) to build {{{}}}'.format(', '.join(TARGETS))) 721 1.1 christos args, extra = parser.parse_known_args(args) 722 1.1 christos args.extra = extra 723 1.1 christos 724 1.1 christos if args.TARGET and args.TARGET not in TARGETS: 725 1.1 christos raise RuntimeError('{} is not a valid target'.format(args.TARGET)) 726 1.1 christos 727 1.1 christos if not args.seed: 728 1.1 christos args.seed = abs_join(CORPORA_DIR, '{}-seed'.format(args.TARGET)) 729 1.1 christos 730 1.1 christos if not os.path.isfile(args.decodecorpus): 731 1.1 christos raise RuntimeError("{} is not a file run 'make -C {} decodecorpus'". 732 1.1 christos format(args.decodecorpus, abs_join(FUZZ_DIR, '..'))) 733 1.1 christos 734 1.1 christos return args 735 1.1 christos 736 1.1 christos 737 1.1 christos def gen(args): 738 1.1 christos try: 739 1.1 christos args = gen_parser(args) 740 1.1 christos except Exception as e: 741 1.1 christos print(e) 742 1.1 christos return 1 743 1.1 christos 744 1.1 christos seed = create(args.seed) 745 1.1 christos with tmpdir() as compressed, tmpdir() as decompressed, tmpdir() as dict: 746 1.1 christos info = TARGET_INFO[args.TARGET] 747 1.1 christos 748 1.1 christos if info.input_type == InputType.DICTIONARY_DATA: 749 1.1 christos number = max(args.number, 1000) 750 1.1 christos else: 751 1.1 christos number = args.number 752 1.1 christos cmd = [ 753 1.1 christos args.decodecorpus, 754 1.1 christos '-n{}'.format(args.number), 755 1.1 christos '-p{}/'.format(compressed), 756 1.1 christos '-o{}'.format(decompressed), 757 1.1 christos ] 758 1.1 christos 759 1.1 christos if info.frame_type == FrameType.BLOCK: 760 1.1 christos cmd += [ 761 1.1 christos '--gen-blocks', 762 1.1 christos '--max-block-size-log={}'.format(min(args.max_size_log, 17)) 763 1.1 christos ] 764 1.1 christos else: 765 1.1 christos cmd += ['--max-content-size-log={}'.format(args.max_size_log)] 766 1.1 christos 767 1.1 christos print(' '.join(cmd)) 768 1.1 christos subprocess.check_call(cmd) 769 1.1 christos 770 1.1 christos if info.input_type == InputType.RAW_DATA: 771 1.1 christos print('using decompressed data in {}'.format(decompressed)) 772 1.1 christos samples = decompressed 773 1.1 christos elif info.input_type == InputType.COMPRESSED_DATA: 774 1.1 christos print('using compressed data in {}'.format(compressed)) 775 1.1 christos samples = compressed 776 1.1 christos else: 777 1.1 christos assert info.input_type == InputType.DICTIONARY_DATA 778 1.1 christos print('making dictionary data from {}'.format(decompressed)) 779 1.1 christos samples = dict 780 1.1 christos min_dict_size_log = 9 781 1.1 christos max_dict_size_log = max(min_dict_size_log + 1, args.max_size_log) 782 1.1 christos for dict_size_log in range(min_dict_size_log, max_dict_size_log): 783 1.1 christos dict_size = 1 << dict_size_log 784 1.1 christos cmd = [ 785 1.1 christos args.zstd, 786 1.1 christos '--train', 787 1.1 christos '-r', decompressed, 788 1.1 christos '--maxdict={}'.format(dict_size), 789 1.1 christos '-o', abs_join(dict, '{}.zstd-dict'.format(dict_size)) 790 1.1 christos ] 791 1.1 christos print(' '.join(cmd)) 792 1.1 christos subprocess.check_call(cmd) 793 1.1 christos 794 1.1 christos # Copy the samples over and prepend the RNG seeds 795 1.1 christos for name in os.listdir(samples): 796 1.1 christos samplename = abs_join(samples, name) 797 1.1 christos outname = abs_join(seed, name) 798 1.1 christos with open(samplename, 'rb') as sample: 799 1.1 christos with open(outname, 'wb') as out: 800 1.1 christos CHUNK_SIZE = 131072 801 1.1 christos chunk = sample.read(CHUNK_SIZE) 802 1.1 christos while len(chunk) > 0: 803 1.1 christos out.write(chunk) 804 1.1 christos chunk = sample.read(CHUNK_SIZE) 805 1.1 christos return 0 806 1.1 christos 807 1.1 christos 808 1.1 christos def minimize(args): 809 1.1 christos try: 810 1.1 christos description = """ 811 1.1 christos Runs a libfuzzer fuzzer with -merge=1 to build a minimal corpus in 812 1.1 christos TARGET_seed_corpus. All extra args are passed to libfuzzer. 813 1.1 christos """ 814 1.1 christos args = targets_parser(args, description) 815 1.1 christos except Exception as e: 816 1.1 christos print(e) 817 1.1 christos return 1 818 1.1 christos 819 1.1 christos for target in args.TARGET: 820 1.1 christos # Merge the corpus + anything else into the seed_corpus 821 1.1 christos corpus = abs_join(CORPORA_DIR, target) 822 1.1 christos seed_corpus = abs_join(CORPORA_DIR, "{}_seed_corpus".format(target)) 823 1.1 christos extra_args = [corpus, "-merge=1"] + args.extra 824 1.1 christos libfuzzer(target, corpora=seed_corpus, extra_args=extra_args) 825 1.1 christos seeds = set(os.listdir(seed_corpus)) 826 1.1 christos # Copy all crashes directly into the seed_corpus if not already present 827 1.1 christos crashes = abs_join(CORPORA_DIR, '{}-crash'.format(target)) 828 1.1 christos for crash in os.listdir(crashes): 829 1.1 christos if crash not in seeds: 830 1.1 christos shutil.copy(abs_join(crashes, crash), seed_corpus) 831 1.1 christos seeds.add(crash) 832 1.1 christos 833 1.1 christos 834 1.1 christos def zip_cmd(args): 835 1.1 christos try: 836 1.1 christos description = """ 837 1.1 christos Zips up the seed corpus. 838 1.1 christos """ 839 1.1 christos args = targets_parser(args, description) 840 1.1 christos except Exception as e: 841 1.1 christos print(e) 842 1.1 christos return 1 843 1.1 christos 844 1.1 christos for target in args.TARGET: 845 1.1 christos # Zip the seed_corpus 846 1.1 christos seed_corpus = abs_join(CORPORA_DIR, "{}_seed_corpus".format(target)) 847 1.1 christos zip_file = "{}.zip".format(seed_corpus) 848 1.1 christos cmd = ["zip", "-r", "-q", "-j", "-9", zip_file, "."] 849 1.1 christos print(' '.join(cmd)) 850 1.1 christos subprocess.check_call(cmd, cwd=seed_corpus) 851 1.1 christos 852 1.1 christos 853 1.1 christos def list_cmd(args): 854 1.1 christos print("\n".join(TARGETS)) 855 1.1 christos 856 1.1 christos 857 1.1 christos def short_help(args): 858 1.1 christos name = args[0] 859 1.1 christos print("Usage: {} [OPTIONS] COMMAND [ARGS]...\n".format(name)) 860 1.1 christos 861 1.1 christos 862 1.1 christos def help(args): 863 1.1 christos short_help(args) 864 1.1 christos print("\tfuzzing helpers (select a command and pass -h for help)\n") 865 1.1 christos print("Options:") 866 1.1 christos print("\t-h, --help\tPrint this message") 867 1.1 christos print("") 868 1.1 christos print("Commands:") 869 1.1 christos print("\tbuild\t\tBuild a fuzzer") 870 1.1 christos print("\tlibfuzzer\tRun a libFuzzer fuzzer") 871 1.1 christos print("\tafl\t\tRun an AFL fuzzer") 872 1.1 christos print("\tregression\tRun a regression test") 873 1.1 christos print("\tgen\t\tGenerate a seed corpus for a fuzzer") 874 1.1 christos print("\tminimize\tMinimize the test corpora") 875 1.1 christos print("\tzip\t\tZip the minimized corpora up") 876 1.1 christos print("\tlist\t\tList the available targets") 877 1.1 christos 878 1.1 christos 879 1.1 christos def main(): 880 1.1 christos args = sys.argv 881 1.1 christos if len(args) < 2: 882 1.1 christos help(args) 883 1.1 christos return 1 884 1.1 christos if args[1] == '-h' or args[1] == '--help' or args[1] == '-H': 885 1.1 christos help(args) 886 1.1 christos return 1 887 1.1 christos command = args.pop(1) 888 1.1 christos args[0] = "{} {}".format(args[0], command) 889 1.1 christos if command == "build": 890 1.1 christos return build(args) 891 1.1 christos if command == "libfuzzer": 892 1.1 christos return libfuzzer_cmd(args) 893 1.1 christos if command == "regression": 894 1.1 christos return regression(args) 895 1.1 christos if command == "afl": 896 1.1 christos return afl(args) 897 1.1 christos if command == "gen": 898 1.1 christos return gen(args) 899 1.1 christos if command == "minimize": 900 1.1 christos return minimize(args) 901 1.1 christos if command == "zip": 902 1.1 christos return zip_cmd(args) 903 1.1 christos if command == "list": 904 1.1 christos return list_cmd(args) 905 1.1 christos short_help(args) 906 1.1 christos print("Error: No such command {} (pass -h for help)".format(command)) 907 1.1 christos return 1 908 1.1 christos 909 1.1 christos 910 1.1 christos if __name__ == "__main__": 911 1.1 christos sys.exit(main()) 912