fuzz.py revision 1.1 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