#!/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 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 . """ 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 '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: # # # # 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()