llvm-project
350 строк · 12.8 Кб
1#!/usr/bin/env python
2
3"""
4This is a generic fuzz testing tool, see --help for more information.
5"""
6
7import os8import sys9import random10import subprocess11import itertools12
13class TestGenerator:14def __init__(self, inputs, delete, insert, replace,15insert_strings, pick_input):16self.inputs = [(s, open(s).read()) for s in inputs]17
18self.delete = bool(delete)19self.insert = bool(insert)20self.replace = bool(replace)21self.pick_input = bool(pick_input)22self.insert_strings = list(insert_strings)23
24self.num_positions = sum([len(d) for _,d in self.inputs])25self.num_insert_strings = len(insert_strings)26self.num_tests = ((delete + (insert + replace)*self.num_insert_strings)27* self.num_positions)28self.num_tests += 129
30if self.pick_input:31self.num_tests *= self.num_positions32
33def position_to_source_index(self, position):34for i,(s,d) in enumerate(self.inputs):35n = len(d)36if position < n:37return (i,position)38position -= n39raise ValueError,'Invalid position.'40
41def get_test(self, index):42assert 0 <= index < self.num_tests43
44picked_position = None45if self.pick_input:46index,picked_position = divmod(index, self.num_positions)47picked_position = self.position_to_source_index(picked_position)48
49if index == 0:50return ('nothing', None, None, picked_position)51
52index -= 153index,position = divmod(index, self.num_positions)54position = self.position_to_source_index(position)55if self.delete:56if index == 0:57return ('delete', position, None, picked_position)58index -= 159
60index,insert_index = divmod(index, self.num_insert_strings)61insert_str = self.insert_strings[insert_index]62if self.insert:63if index == 0:64return ('insert', position, insert_str, picked_position)65index -= 166
67assert self.replace68assert index == 069return ('replace', position, insert_str, picked_position)70
71class TestApplication:72def __init__(self, tg, test):73self.tg = tg74self.test = test75
76def apply(self):77if self.test[0] == 'nothing':78pass79else:80i,j = self.test[1]81name,data = self.tg.inputs[i]82if self.test[0] == 'delete':83data = data[:j] + data[j+1:]84elif self.test[0] == 'insert':85data = data[:j] + self.test[2] + data[j:]86elif self.test[0] == 'replace':87data = data[:j] + self.test[2] + data[j+1:]88else:89raise ValueError,'Invalid test %r' % self.test90open(name,'wb').write(data)91
92def revert(self):93if self.test[0] != 'nothing':94i,j = self.test[1]95name,data = self.tg.inputs[i]96open(name,'wb').write(data)97
98def quote(str):99return '"' + str + '"'100
101def run_one_test(test_application, index, input_files, args):102test = test_application.test103
104# Interpolate arguments.105options = { 'index' : index,106'inputs' : ' '.join(quote(f) for f in input_files) }107
108# Add picked input interpolation arguments, if used.109if test[3] is not None:110pos = test[3][1]111options['picked_input'] = input_files[test[3][0]]112options['picked_input_pos'] = pos113# Compute the line and column.114file_data = test_application.tg.inputs[test[3][0]][1]115line = column = 1116for i in range(pos):117c = file_data[i]118if c == '\n':119line += 1120column = 1121else:122column += 1123options['picked_input_line'] = line124options['picked_input_col'] = column125
126test_args = [a % options for a in args]127if opts.verbose:128print '%s: note: executing %r' % (sys.argv[0], test_args)129
130stdout = None131stderr = None132if opts.log_dir:133stdout_log_path = os.path.join(opts.log_dir, '%s.out' % index)134stderr_log_path = os.path.join(opts.log_dir, '%s.err' % index)135stdout = open(stdout_log_path, 'wb')136stderr = open(stderr_log_path, 'wb')137else:138sys.stdout.flush()139p = subprocess.Popen(test_args, stdout=stdout, stderr=stderr)140p.communicate()141exit_code = p.wait()142
143test_result = (exit_code == opts.expected_exit_code or144exit_code in opts.extra_exit_codes)145
146if stdout is not None:147stdout.close()148stderr.close()149
150# Remove the logs for passes, unless logging all results.151if not opts.log_all and test_result:152os.remove(stdout_log_path)153os.remove(stderr_log_path)154
155if not test_result:156print 'FAIL: %d' % index157elif not opts.succinct:158print 'PASS: %d' % index159return test_result160
161def main():162global opts163from optparse import OptionParser, OptionGroup164parser = OptionParser("""%prog [options] ... test command args ...165
166%prog is a tool for fuzzing inputs and testing them.
167
168The most basic usage is something like:
169
170$ %prog --file foo.txt ./test.sh
171
172which will run a default list of fuzzing strategies on the input. For each
173fuzzed input, it will overwrite the input files (in place), run the test script,
174then restore the files back to their original contents.
175
176NOTE: You should make sure you have a backup copy of your inputs, in case
177something goes wrong!!!
178
179You can cause the fuzzing to not restore the original files with
180'--no-revert'. Generally this is used with '--test <index>' to run one failing
181test and then leave the fuzzed inputs in place to examine the failure.
182
183For each fuzzed input, %prog will run the test command given on the command
184line. Each argument in the command is subject to string interpolation before
185being executed. The syntax is "%(VARIABLE)FORMAT" where FORMAT is a standard
186printf format, and VARIABLE is one of:
187
188'index' - the test index being run
189'inputs' - the full list of test inputs
190'picked_input' - (with --pick-input) the selected input file
191'picked_input_pos' - (with --pick-input) the selected input position
192'picked_input_line' - (with --pick-input) the selected input line
193'picked_input_col' - (with --pick-input) the selected input column
194
195By default, the script will run forever continually picking new tests to
196run. You can limit the number of tests that are run with '--max-tests <number>',
197and you can run a particular test with '--test <index>'.
198
199You can specify '--stop-on-fail' to stop the script on the first failure
200without reverting the changes.
201
202""")203parser.add_option("-v", "--verbose", help="Show more output",204action='store_true', dest="verbose", default=False)205parser.add_option("-s", "--succinct", help="Reduce amount of output",206action="store_true", dest="succinct", default=False)207
208group = OptionGroup(parser, "Test Execution")209group.add_option("", "--expected-exit-code", help="Set expected exit code",210type=int, dest="expected_exit_code",211default=0)212group.add_option("", "--extra-exit-code",213help="Set additional expected exit code",214type=int, action="append", dest="extra_exit_codes",215default=[])216group.add_option("", "--log-dir",217help="Capture test logs to an output directory",218type=str, dest="log_dir",219default=None)220group.add_option("", "--log-all",221help="Log all outputs (not just failures)",222action="store_true", dest="log_all", default=False)223parser.add_option_group(group)224
225group = OptionGroup(parser, "Input Files")226group.add_option("", "--file", metavar="PATH",227help="Add an input file to fuzz",228type=str, action="append", dest="input_files", default=[])229group.add_option("", "--filelist", metavar="LIST",230help="Add a list of inputs files to fuzz (one per line)",231type=str, action="append", dest="filelists", default=[])232parser.add_option_group(group)233
234group = OptionGroup(parser, "Fuzz Options")235group.add_option("", "--replacement-chars", dest="replacement_chars",236help="Characters to insert/replace",237default="0{}[]<>\;@#$^%& ")238group.add_option("", "--replacement-string", dest="replacement_strings",239action="append", help="Add a replacement string to use",240default=[])241group.add_option("", "--replacement-list", dest="replacement_lists",242help="Add a list of replacement strings (one per line)",243action="append", default=[])244group.add_option("", "--no-delete", help="Don't delete characters",245action='store_false', dest="enable_delete", default=True)246group.add_option("", "--no-insert", help="Don't insert strings",247action='store_false', dest="enable_insert", default=True)248group.add_option("", "--no-replace", help="Don't replace strings",249action='store_false', dest="enable_replace", default=True)250group.add_option("", "--no-revert", help="Don't revert changes",251action='store_false', dest="revert", default=True)252group.add_option("", "--stop-on-fail", help="Stop on first failure",253action='store_true', dest="stop_on_fail", default=False)254parser.add_option_group(group)255
256group = OptionGroup(parser, "Test Selection")257group.add_option("", "--test", help="Run a particular test",258type=int, dest="test", default=None, metavar="INDEX")259group.add_option("", "--max-tests", help="Maximum number of tests",260type=int, dest="max_tests", default=None, metavar="COUNT")261group.add_option("", "--pick-input",262help="Randomly select an input byte as well as fuzzing",263action='store_true', dest="pick_input", default=False)264parser.add_option_group(group)265
266parser.disable_interspersed_args()267
268(opts, args) = parser.parse_args()269
270if not args:271parser.error("Invalid number of arguments")272
273# Collect the list of inputs.274input_files = list(opts.input_files)275for filelist in opts.filelists:276f = open(filelist)277try:278for ln in f:279ln = ln.strip()280if ln:281input_files.append(ln)282finally:283f.close()284input_files.sort()285
286if not input_files:287parser.error("No input files!")288
289print '%s: note: fuzzing %d files.' % (sys.argv[0], len(input_files))290
291# Make sure the log directory exists if used.292if opts.log_dir:293if not os.path.exists(opts.log_dir):294try:295os.mkdir(opts.log_dir)296except OSError:297print "%s: error: log directory couldn't be created!" % (298sys.argv[0],)299raise SystemExit,1300
301# Get the list if insert/replacement strings.302replacements = list(opts.replacement_chars)303replacements.extend(opts.replacement_strings)304for replacement_list in opts.replacement_lists:305f = open(replacement_list)306try:307for ln in f:308ln = ln[:-1]309if ln:310replacements.append(ln)311finally:312f.close()313
314# Unique and order the replacement list.315replacements = list(set(replacements))316replacements.sort()317
318# Create the test generator.319tg = TestGenerator(input_files, opts.enable_delete, opts.enable_insert,320opts.enable_replace, replacements, opts.pick_input)321
322print '%s: note: %d input bytes.' % (sys.argv[0], tg.num_positions)323print '%s: note: %d total tests.' % (sys.argv[0], tg.num_tests)324if opts.test is not None:325it = [opts.test]326elif opts.max_tests is not None:327it = itertools.imap(random.randrange,328itertools.repeat(tg.num_tests, opts.max_tests))329else:330it = itertools.imap(random.randrange, itertools.repeat(tg.num_tests))331for test in it:332t = tg.get_test(test)333
334if opts.verbose:335print '%s: note: running test %d: %r' % (sys.argv[0], test, t)336ta = TestApplication(tg, t)337try:338ta.apply()339test_result = run_one_test(ta, test, input_files, args)340if not test_result and opts.stop_on_fail:341opts.revert = False342sys.exit(1)343finally:344if opts.revert:345ta.revert()346
347sys.stdout.flush()348
349if __name__ == '__main__':350main()351