#!/usr/bin/env python # Copyright (C) 2018-2019 Cisco Systems, Inc. and/or its affiliates. All rights reserved. ''' This script is a convenience tool to run a standalone fuzz target against each item in its associated fuzz corpus. ''' from __future__ import print_function, division, absolute_import import argparse import os import subprocess import sys import tempfile import threading def which(program): ''' Implements bash "which" feature. Find the full path to a program located in the PATH. https://stackoverflow.com/a/377028 ''' def is_exe(fpath): return os.path.isfile(fpath) and os.access(fpath, os.X_OK) fpath, _ = os.path.split(program) if fpath: if is_exe(program): return program else: for path in os.environ["PATH"].split(os.pathsep): exe_file = os.path.join(path, program) if is_exe(exe_file): return exe_file return None def cmd(command): ''' Run a command in a subprocess. https://stackoverflow.com/a/4408409 https://stackoverflow.com/a/10012262 ''' with tempfile.TemporaryFile() as tempf: p = subprocess.Popen(command, stderr=tempf) is_killed = {'value': False} def timeout(p, is_killed): is_killed['value'] = True p.kill() timer = threading.Timer(2, timeout, [p, is_killed]) try: timer.start() p.wait() tempf.seek(0) text = tempf.read().decode("utf-8").strip() returncode = p.returncode finally: timer.cancel() if is_killed['value']: text = 'error: timeout, ' + text returncode = 1 return text, returncode def run_test(fuzzer, corpus_path): ''' Test a standalone fuzz target with each item from the fuzz corpus. ''' builddir = os.environ.get("builddir", ".") fuzz_target = os.path.join(builddir, fuzzer) print("Fuzz Target: {fuzzer}".format(fuzzer=fuzzer)) print("Corpus Path: {corpus_path}".format(corpus_path=corpus_path)) if not os.path.exists(fuzz_target): print("Failed to find fuzz target: {binary}!".format(binary=fuzz_target)) sys.exit(1) failures = 0 valgrind = None if os.environ.get('VG', ''): valgrind = which('valgrind') for fname in os.listdir(corpus_path): seedpath = os.path.join(corpus_path, fname) text, returncode = cmd([fuzz_target, seedpath]) if text.strip(): print(text) failed = False if returncode != 0 or 'error' in text: print('failure on %s' % fname) failed = True if valgrind: text, returncode = cmd( [valgrind, '--error-exitcode=1', fuzz_target, seedpath]) if returncode: print(text) print('failure on %s' % fname) failed = True if failed: failures = failures + 1 if failures: print("%i scanfile fuzzer related tests failed." % failures) sys.exit(1) def main(): ''' Get command line options to support this tool. ''' parser = argparse.ArgumentParser(description=__doc__) parser.add_argument( '-f', '--fuzzer', required=True, help="The fuzz target to test.") parser.add_argument( '-c', '--corpus', required=True, help="Path of the fuzz corpus.") args = parser.parse_args() run_test(args.fuzzer, args.corpus) if __name__ == '__main__': main()