| #!/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() |