45d5b483da
Previously, the minimizer only handled a small, handpicked set of types when replacing expressions with minimized literals of the expression's type. This CL adds minimal literal generation for any type that has a known constructor and generalizes handling of lists, sets, and maps. TEST=dartfuzz Change-Id: Ifeaacfd4dec1ba8f66f4fee9cf8c7fb74193b0d2 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/403861 Reviewed-by: Ryan Macnak <rmacnak@google.com> Commit-Queue: Tess Strickland <sstrickl@google.com>
415 lines
15 KiB
Python
Executable File
415 lines
15 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
# Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file
|
|
# for details. All rights reserved. Use of this source code is governed by a
|
|
# BSD-style license that can be found in the LICENSE file.
|
|
#
|
|
"""
|
|
This script minimizes a program generated by dartfuzz.dart.
|
|
"""
|
|
|
|
import argparse
|
|
import multiprocessing as mp
|
|
import re
|
|
import shlex
|
|
import subprocess
|
|
|
|
STATEMENT_MASK_LINE = 0
|
|
STATEMENT_NUMBER_LINE = 1
|
|
EXPRESSION_MASK_LINE = 2
|
|
EXPRESSION_NUMBER_LINE = 3
|
|
|
|
|
|
class MaskGen(object):
|
|
"""
|
|
Generates bitpatterns of <nbits> bits which indicate which
|
|
statements or expressions should be masked when passed as the
|
|
--smask or --emask parameter to dartfuzz.dart.
|
|
The patterns are generated in the following manner:
|
|
11111111111111111111111111111111
|
|
11111111111111110000000000000000
|
|
00000000000000001111111111111111
|
|
11111111111111111111111100000000
|
|
11111111111111110000000011111111
|
|
11111111000000001111111111111111
|
|
00000000111111111111111111111111
|
|
...
|
|
If the error persists with a given a pattern it is stored in the
|
|
parameter <mask>.
|
|
"""
|
|
|
|
def __init__(self, nbits, mask):
|
|
# Mask bitlength.
|
|
self.nbits = nbits
|
|
# Mask determining which statements or expressions to skip.
|
|
self.mask = mask
|
|
# Current mask modification start index.
|
|
self.convolution_idx = 0
|
|
# Max mask with all bits set to 1.
|
|
self.max = (1 << (nbits + 1)) - 1
|
|
# Current number of bits set in the mask modification mask.
|
|
self.nbitsVar = int(nbits)
|
|
# Current mask modification mask.
|
|
# This will be folded over the current value of self.mask
|
|
# starting from 0.
|
|
self.filter_mask = (1 << self.nbitsVar) - 1
|
|
# All the masks already tested.
|
|
self.tested = set()
|
|
self.gen_new_mask = False
|
|
|
|
def __iter__(self):
|
|
return self
|
|
|
|
def __next__(self):
|
|
return self.next()
|
|
|
|
# Update the set of tested masks.
|
|
# This is used to keep track of tested masks that did not produce a crash.
|
|
def update_tested(self, new_mask):
|
|
self.tested.add(new_mask)
|
|
|
|
# Update the current mask with the new mask that produced a crash.
|
|
# Note: We assume that if mask
|
|
# 11110000 produces a crash and
|
|
# 00000011 produces a crash that
|
|
# 11110011 will also produce a crash.
|
|
# This might not be true for some cases.
|
|
def update_mask(self, new_mask):
|
|
new_mask = (self.mask | new_mask) & self.max
|
|
self.mask = new_mask
|
|
|
|
# Count the bits of the current mask.
|
|
# More bits mean more statements omitted, so this is used
|
|
# to determine the quality of the current mask.
|
|
def count_bits(self):
|
|
return bin(self.mask).count('1')
|
|
|
|
def inc_var(self):
|
|
self.convolution_idx = 0
|
|
self.nbitsVar = self.nbitsVar >> 1
|
|
self.filter_mask = (1 << self.nbitsVar) - 1
|
|
return self.filter_mask == 0
|
|
|
|
def stop(self):
|
|
if self.convolution_idx >= self.nbits and self.inc_var() > 0:
|
|
# Check if the last run generated a new mask.
|
|
if self.gen_new_mask:
|
|
# Restart mask generation from beginning.
|
|
self.nbitsVar = self.nbits >> 1
|
|
self.inc_var()
|
|
self.gen_new_mask = False
|
|
return False
|
|
print("STOP")
|
|
return True
|
|
return False
|
|
|
|
def next(self):
|
|
while not self.stop():
|
|
# Generate a new mask according to the mask generation algorithm.
|
|
new_mask = (self.mask |
|
|
(self.filter_mask << self.convolution_idx)) & self.max
|
|
# Update the counter variable for the mask generation algorithm.
|
|
self.convolution_idx += self.nbitsVar
|
|
# If the new_mask has new bits and has not been tested yet.
|
|
# We need to check both because only masks that produce an
|
|
# error are merged into self.mask.
|
|
if new_mask != self.mask and \
|
|
new_mask not in self.tested:
|
|
# Add new mask to the set of tested masks.
|
|
self.tested.add(new_mask)
|
|
# Mark that we generated a new mask on this run.
|
|
self.gen_new_mask = True
|
|
# Return the new mask to be tested.
|
|
return new_mask
|
|
raise StopIteration()
|
|
|
|
|
|
# Generate a Dart program for the given statement (smask) and
|
|
# expression (emask) masks. Dartfuzz parameters for seed, ffi and
|
|
# fp are static and therefore contained in the dartfuzz_cmd parameter.
|
|
def generate_dart(dartfuzz_cmd, dart_test, smask, mask, do_expr):
|
|
if do_expr:
|
|
# Minimizing expressions.
|
|
cmds = shlex.split(dartfuzz_cmd) + [dart_test] + \
|
|
['--mini', '--smask', '%d' % smask, '--emask', '%d' % mask]
|
|
p = subprocess.Popen(cmds, stdout=subprocess.PIPE)
|
|
else:
|
|
# Minimizing statements.
|
|
cmds = shlex.split(dartfuzz_cmd) + [dart_test] + \
|
|
['--mini', '--smask', '%d' % mask]
|
|
p = subprocess.Popen(cmds, stdout=subprocess.PIPE)
|
|
p_stdout, p_stderr = p.communicate()
|
|
if p.returncode != 0:
|
|
raise RuntimeError('Invalid return code on generate %d' % p.returncode)
|
|
mask_new = 0
|
|
if do_expr:
|
|
mask_new = int(p_stdout.decode().splitlines()[EXPRESSION_MASK_LINE])
|
|
else:
|
|
mask_new = int(p_stdout.decode().splitlines()[STATEMENT_MASK_LINE])
|
|
return mask_new
|
|
|
|
|
|
# Run the Dart program and check for error (by matching error_match_p).
|
|
def run_dart(dart_cmd, dart_test, error_match_p, timeout):
|
|
cmd = ['timeout', str(timeout)] + shlex.split(dart_cmd) + [dart_test]
|
|
p = subprocess.Popen(cmd, stderr=subprocess.PIPE, stdout=subprocess.PIPE)
|
|
p_stdout, p_stderr = p.communicate()
|
|
if error_match_p.search(p_stdout.decode()) or \
|
|
error_match_p.search(p_stderr.decode()):
|
|
return True
|
|
if p.returncode != 0:
|
|
print("Warning: error return code %d" % p.returncode)
|
|
return False
|
|
|
|
|
|
# Run multiple tries of the given dart_cmd and check for errors by matching
|
|
# against error_match_p.
|
|
def run_dart_mp(dart_cmd, dart_test, error_match_p, tries, nthreads, timeout):
|
|
if tries == 1:
|
|
return run_dart(dart_cmd, dart_test, error_match_p, timeout)
|
|
pool = mp.Pool(nthreads)
|
|
results = [
|
|
pool.apply_async(run_dart,
|
|
(dart_cmd, dart_test, error_match_p, timeout))
|
|
for i in range(tries)
|
|
]
|
|
worker_done = [False for i in range(tries)]
|
|
while True:
|
|
all_done = True
|
|
for i, result in enumerate(results):
|
|
if worker_done[i]:
|
|
continue
|
|
try:
|
|
r = result.get(timeout=0.5)
|
|
worker_done[i] = True
|
|
if r:
|
|
pool.close()
|
|
pool.terminate()
|
|
pool.join()
|
|
return True
|
|
except mp.TimeoutError:
|
|
all_done = False
|
|
if all_done:
|
|
break
|
|
pool.close()
|
|
pool.join()
|
|
return False
|
|
|
|
|
|
def minimize(dartfuzz_cmd,
|
|
dart_cmd,
|
|
dart_cmd_ref,
|
|
dart_test,
|
|
error_match1,
|
|
error_match2,
|
|
smask,
|
|
emask,
|
|
do_expr,
|
|
verbose,
|
|
tries,
|
|
tries_ref,
|
|
threads,
|
|
timeout,
|
|
print_every=32):
|
|
# Run dartfuzz command once to get the total number of statements
|
|
# and expressions in the generated program.
|
|
# Output will look like this:
|
|
# <Current Statement Mask>
|
|
# <Number of Statements>
|
|
# <Current Expression Mask>
|
|
# <Number of Expressions>
|
|
p = subprocess.Popen(
|
|
dartfuzz_cmd + " --mini " + dart_test,
|
|
shell=True,
|
|
stdout=subprocess.PIPE)
|
|
p_stdout, p_stderr = p.communicate()
|
|
if do_expr:
|
|
# Minimizing expressions.
|
|
nstmts = int(p_stdout.decode().splitlines()[EXPRESSION_NUMBER_LINE])
|
|
mask_gen = MaskGen(nstmts, emask)
|
|
else:
|
|
# Minimizing statements.
|
|
nstmts = int(p_stdout.decode().splitlines()[STATEMENT_NUMBER_LINE])
|
|
mask_gen = MaskGen(nstmts, smask)
|
|
# Best mask found so far.
|
|
min_mask = 0
|
|
# Maximum number of statements or expressions masked.
|
|
max_bits = 0
|
|
# Count number of iterations.
|
|
cntr = 0
|
|
# Result of the last run.
|
|
last_err = True
|
|
# Compile pattern to match standard output and error for a string
|
|
# identifying the crash.
|
|
error_match_p1 = re.compile(error_match1)
|
|
error_match_p2 = None
|
|
if error_match2 is not None:
|
|
error_match_p2 = re.compile(error_match2)
|
|
for mask in mask_gen:
|
|
if (verbose):
|
|
print("Mask: %x" % mask)
|
|
cntr += 1
|
|
if cntr % print_every == 0:
|
|
cntr = 0
|
|
print("Best I could do so far is mask %d/%d" \
|
|
% (max_bits, nstmts))
|
|
if do_expr:
|
|
print(dartfuzz_cmd + " " + dart_test +
|
|
" --mini --smask 0x%x --emask 0x%x" % (smask, min_mask))
|
|
else:
|
|
print(dartfuzz_cmd + " " + dart_test +
|
|
" --mini --smask 0x%x --emask 0" % (min_mask))
|
|
# The return value mask_new contains the actual mask applied by
|
|
# dartfuzz. Due to nesting, masking one statement might lead to other
|
|
# statements being masked as well; mask_new will contain this updated
|
|
# mask.
|
|
mask_new = generate_dart(dartfuzz_cmd, dart_test, smask, mask, do_expr)
|
|
# Run generated Dart program and check for error.
|
|
err = run_dart_mp(dart_cmd, dart_test, error_match_p1, tries, threads,
|
|
timeout)
|
|
if err and verbose:
|
|
print("Matched error 1 " + error_match1)
|
|
err_ref = True
|
|
if (dart_cmd_ref is not None) and (error_match_p2 is not None):
|
|
err_ref = run_dart_mp(dart_cmd_ref, dart_test, error_match_p2,
|
|
tries_ref, threads, timeout)
|
|
if err_ref and verbose:
|
|
print("Matched error 2 " + error_match2)
|
|
if err and err_ref:
|
|
# In case the mask used for generating the Dart program lead to an
|
|
# error, update the mask generator accordingly.
|
|
mask_gen.update_mask(mask)
|
|
# TODO (felih): this line should be enough but there seems to be a
|
|
# bug in dartfuzz.dart calculating mask_new.
|
|
mask_gen.update_mask(mask_new)
|
|
max_bits = mask_gen.count_bits()
|
|
min_mask = mask_gen.mask
|
|
elif last_err:
|
|
# If the last run removed the error try the inverse.
|
|
invMaskNew = mask_gen.mask | (mask_gen.max & ~mask_new)
|
|
if invMaskNew != mask_gen.mask and \
|
|
invMaskNew not in mask_gen.tested:
|
|
if (verbose):
|
|
print("Mask: %x (i)" % invMaskNew)
|
|
mask_new = generate_dart(dartfuzz_cmd, dart_test, smask,
|
|
invMaskNew, do_expr)
|
|
err = run_dart_mp(dart_cmd, dart_test, error_match_p1, tries,
|
|
threads, timeout)
|
|
if err and verbose:
|
|
print("Matched error 1 " + error_match1)
|
|
err_ref = True
|
|
if (dart_cmd_ref is not None) and (error_match_p2 is not None):
|
|
err_ref = run_dart_mp(dart_cmd_ref, dart_test,
|
|
error_match_p2, tries_ref, threads,
|
|
timeout)
|
|
if err_ref and verbose:
|
|
print("Matched error 2 " + error_match2)
|
|
if err and err_ref:
|
|
mask_gen.update_mask(invMaskNew)
|
|
mask_gen.update_mask(mask_new)
|
|
max_bits = mask_gen.count_bits()
|
|
min_mask = mask_gen.mask
|
|
last_err = err and err_ref
|
|
# Update the set of tested masks with the one we just tested.
|
|
mask_gen.update_tested(mask_new)
|
|
|
|
print("Best I could do is %d/%d" \
|
|
% (max_bits,nstmts))
|
|
|
|
if do_expr:
|
|
print(dartfuzz_cmd + " " + dart_test +
|
|
" --mini --smask 0x%x --emask 0x%x" % (smask, min_mask))
|
|
else:
|
|
print(dartfuzz_cmd + " " + dart_test +
|
|
" --mini --smask 0x%x --emask 0" % (min_mask))
|
|
|
|
return min_mask
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(
|
|
description='Minimize a generated Dart program ' +
|
|
' while maintaining an identified crash or divergence. ' +
|
|
'A second Dart program can be specified for divergence testing.')
|
|
parser.add_argument(
|
|
'--dartfuzz',
|
|
required=True,
|
|
help="dartfuzz command string "
|
|
"e.g. dart dartfuzz.dart --no-ffi --no-fp --seed 243123600")
|
|
parser.add_argument(
|
|
'--dart',
|
|
help="Dart command string "
|
|
"e.g. ./sdk/out/ReleaseX64/dart",
|
|
required=True)
|
|
parser.add_argument(
|
|
'--dart-ref',
|
|
dest="dart_ref",
|
|
help="Dart command string for reference build " +
|
|
"(in case of divergence testing) " + "e.g. ./sdk/out/ReleaseX64/dart",
|
|
default=None)
|
|
parser.add_argument(
|
|
'--testfile',
|
|
required=True,
|
|
help="output filename for program generated by "
|
|
"dartfuzz command and passed to Dart command "
|
|
"e.g fuzz.dart")
|
|
parser.add_argument(
|
|
'--err',
|
|
help="string indicating an error for Dart cmd (no multiline matches)",
|
|
required=True)
|
|
parser.add_argument(
|
|
'--err-ref',
|
|
dest="err_ref",
|
|
help="string matching the diverging output for the Dart " +
|
|
"reference command (no multiline matches)",
|
|
default=None)
|
|
parser.add_argument(
|
|
'--smask', help="hexadecimal statements mask", default="0")
|
|
parser.add_argument(
|
|
'--emask', help="hexadecimal expressions mask", default="0")
|
|
parser.add_argument(
|
|
'--typ',
|
|
choices=["s", "e", "se"],
|
|
required=True,
|
|
help="minimize statements (s) or expressions (e) or both (se)")
|
|
parser.add_argument(
|
|
'--tries',
|
|
type=int,
|
|
default=1,
|
|
help='number of retries per run for Dart cmd')
|
|
parser.add_argument(
|
|
'--tries-ref',
|
|
type=int,
|
|
dest='tries_ref',
|
|
default=1,
|
|
help='number of retries per run for Dart reference cmd')
|
|
parser.add_argument(
|
|
'--threads',
|
|
type=int,
|
|
default=4,
|
|
help='number of threads to use for retries')
|
|
parser.add_argument(
|
|
'--timeout', type=int, default=60, help='timeout for Dart command')
|
|
parser.add_argument(
|
|
'--verbose', help="print intermediate results", action="store_true")
|
|
args = parser.parse_args()
|
|
timeout = args.timeout
|
|
do_stmt = "s" in args.typ
|
|
do_expr = "e" in args.typ
|
|
smask = int(args.smask, 16)
|
|
if do_stmt:
|
|
print("Minimizing Statements")
|
|
smask = minimize(args.dartfuzz, args.dart, args.dart_ref,
|
|
args.testfile, args.err, args.err_ref, smask,
|
|
int(args.emask, 16), False, args.verbose, args.tries,
|
|
args.tries_ref, args.threads, timeout)
|
|
if do_expr:
|
|
print("Minimizing Expressions")
|
|
minimize(args.dartfuzz, args.dart, args.dart_ref,
|
|
args.testfile, args.err, args.err_ref, smask,
|
|
int(args.emask, 16), True, args.verbose, args.tries,
|
|
args.tries_ref, args.threads, timeout)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|