#!/usr/bin/env python3 # # Copyright (C) 2016 The Android Open Source Project # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import argparse import os import shutil import sys from glob import glob from subprocess import call from tempfile import mkdtemp sys.path.append(os.path.dirname(os.path.dirname( os.path.realpath(__file__)))) from common.common import FatalError from common.common import GetEnvVariableOrError from common.common import RetCode from common.common import RunCommand # # Tester class. # class DexFuzzTester(object): """Tester that feeds JFuzz programs into DexFuzz testing.""" def __init__(self, num_tests, num_inputs, device, dexer, debug_info): """Constructor for the tester. Args: num_tests: int, number of tests to run num_inputs: int, number of JFuzz programs to generate device: string, target device serial number (or None) dexer: string, defines dexer debug_info: boolean, if True include debugging info """ self._num_tests = num_tests self._num_inputs = num_inputs self._device = device self._save_dir = None self._results_dir = None self._dexfuzz_dir = None self._inputs_dir = None self._dexfuzz_env = None self._dexer = dexer self._debug_info = debug_info def __enter__(self): """On entry, enters new temp directory after saving current directory. Raises: FatalError: error when temp directory cannot be constructed """ self._save_dir = os.getcwd() self._results_dir = mkdtemp(dir='/tmp/') self._dexfuzz_dir = mkdtemp(dir=self._results_dir) self._inputs_dir = mkdtemp(dir=self._dexfuzz_dir) if self._results_dir is None or self._dexfuzz_dir is None or \ self._inputs_dir is None: raise FatalError('Cannot obtain temp directory') self._dexfuzz_env = os.environ.copy() self._dexfuzz_env['ANDROID_DATA'] = self._dexfuzz_dir top = GetEnvVariableOrError('ANDROID_BUILD_TOP') self._dexfuzz_env['PATH'] = (top + '/art/tools/bisection_search:' + self._dexfuzz_env['PATH']) android_root = GetEnvVariableOrError('ANDROID_HOST_OUT') self._dexfuzz_env['ANDROID_ROOT'] = android_root self._dexfuzz_env['LD_LIBRARY_PATH'] = android_root + '/lib' os.chdir(self._dexfuzz_dir) os.mkdir('divergent_programs') os.mkdir('bisection_outputs') return self def __exit__(self, etype, evalue, etraceback): """On exit, re-enters previously saved current directory and cleans up.""" os.chdir(self._save_dir) # TODO: detect divergences or shutil.rmtree(self._results_dir) def Run(self): """Feeds JFuzz programs into DexFuzz testing.""" print() print('**\n**** J/DexFuzz Testing\n**') print() print('#Tests :', self._num_tests) print('Device :', self._device) print('Directory :', self._results_dir) print() self.GenerateJFuzzPrograms() self.RunDexFuzz() def CompileOnHost(self): """Compiles Test.java into classes.dex using either javac/dx or d8. Raises: FatalError: error when compilation fails """ if self._dexer == 'dx' or self._dexer == 'd8': dbg = '-g' if self._debug_info else '-g:none' if RunCommand(['javac', '--release=8', dbg, 'Test.java'], out=None, err='jerr.txt', timeout=30) != RetCode.SUCCESS: print('Unexpected error while running javac') raise FatalError('Unexpected error while running javac') cfiles = glob('*.class') dx = 'dx' if self._dexer == 'dx' else 'd8-compat-dx' if RunCommand([dx, '--dex', '--output=classes.dex'] + cfiles, out=None, err='dxerr.txt', timeout=30) != RetCode.SUCCESS: print('Unexpected error while running dx') raise FatalError('Unexpected error while running dx') # Cleanup on success (nothing to see). for cfile in cfiles: os.unlink(cfile) os.unlink('jerr.txt') os.unlink('dxerr.txt') else: raise FatalError('Unknown dexer: ' + self._dexer) def GenerateJFuzzPrograms(self): """Generates JFuzz programs. Raises: FatalError: error when generation fails """ os.chdir(self._inputs_dir) for i in range(1, self._num_inputs + 1): if RunCommand(['jfuzz'], out='Test.java', err=None) != RetCode.SUCCESS: print('Unexpected error while running JFuzz') raise FatalError('Unexpected error while running JFuzz') self.CompileOnHost() shutil.move('Test.java', '../Test' + str(i) + '.java') shutil.move('classes.dex', 'classes' + str(i) + '.dex') def RunDexFuzz(self): """Starts the DexFuzz testing.""" os.chdir(self._dexfuzz_dir) dexfuzz_args = ['--inputs=' + self._inputs_dir, '--execute', '--execute-class=Test', '--repeat=' + str(self._num_tests), '--quiet', '--dump-output', '--dump-verify', '--interpreter', '--optimizing', '--bisection-search'] if self._device is not None: dexfuzz_args += ['--device=' + self._device, '--allarm'] else: dexfuzz_args += ['--host'] # Assume host otherwise. cmd = ['dexfuzz'] + dexfuzz_args print('**** Running ****\n\n', cmd, '\n') call(cmd, env=self._dexfuzz_env) print('\n**** Results (report.log) ****\n') call(['tail', '-n 24', 'report.log']) def main(): # Handle arguments. parser = argparse.ArgumentParser() parser.add_argument('--num_tests', default=1000, type=int, help='number of tests to run (default: 1000)') parser.add_argument('--num_inputs', default=10, type=int, help='number of JFuzz program to generate (default: 10)') parser.add_argument('--device', help='target device serial number') parser.add_argument('--dexer', default='dx', type=str, help='defines dexer as dx or d8 (default: dx)') parser.add_argument('--debug_info', default=False, action='store_true', help='include debugging info') args = parser.parse_args() # Run the DexFuzz tester. with DexFuzzTester(args.num_tests, args.num_inputs, args.device, args.dexer, args.debug_info) as fuzzer: fuzzer.Run() if __name__ == '__main__': main()