1#!/usr/bin/env python3
2#
3# Copyright (C) 2016 The Android Open Source Project
4#
5# Licensed under the Apache License, Version 2.0 (the "License");
6# you may not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8#
9#   http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16
17import argparse
18import os
19import shutil
20import sys
21
22from glob import glob
23from subprocess import call
24from tempfile import mkdtemp
25
26sys.path.append(os.path.dirname(os.path.dirname(
27        os.path.realpath(__file__))))
28
29from common.common import FatalError
30from common.common import GetEnvVariableOrError
31from common.common import RetCode
32from common.common import RunCommand
33
34
35#
36# Tester class.
37#
38
39
40class DexFuzzTester(object):
41  """Tester that feeds JFuzz programs into DexFuzz testing."""
42
43  def  __init__(self, num_tests, num_inputs, device, dexer, debug_info):
44    """Constructor for the tester.
45
46    Args:
47      num_tests: int, number of tests to run
48      num_inputs: int, number of JFuzz programs to generate
49      device: string, target device serial number (or None)
50      dexer: string, defines dexer
51      debug_info: boolean, if True include debugging info
52    """
53    self._num_tests = num_tests
54    self._num_inputs = num_inputs
55    self._device = device
56    self._save_dir = None
57    self._results_dir = None
58    self._dexfuzz_dir = None
59    self._inputs_dir = None
60    self._dexfuzz_env = None
61    self._dexer = dexer
62    self._debug_info = debug_info
63
64  def __enter__(self):
65    """On entry, enters new temp directory after saving current directory.
66
67    Raises:
68      FatalError: error when temp directory cannot be constructed
69    """
70    self._save_dir = os.getcwd()
71    self._results_dir = mkdtemp(dir='/tmp/')
72    self._dexfuzz_dir = mkdtemp(dir=self._results_dir)
73    self._inputs_dir = mkdtemp(dir=self._dexfuzz_dir)
74    if self._results_dir is None or self._dexfuzz_dir is None or \
75        self._inputs_dir is None:
76      raise FatalError('Cannot obtain temp directory')
77    self._dexfuzz_env = os.environ.copy()
78    self._dexfuzz_env['ANDROID_DATA'] = self._dexfuzz_dir
79    top = GetEnvVariableOrError('ANDROID_BUILD_TOP')
80    self._dexfuzz_env['PATH'] = (top + '/art/tools/bisection_search:' +
81                                 self._dexfuzz_env['PATH'])
82    android_root = GetEnvVariableOrError('ANDROID_HOST_OUT')
83    self._dexfuzz_env['ANDROID_ROOT'] = android_root
84    self._dexfuzz_env['LD_LIBRARY_PATH'] = android_root + '/lib'
85    os.chdir(self._dexfuzz_dir)
86    os.mkdir('divergent_programs')
87    os.mkdir('bisection_outputs')
88    return self
89
90  def __exit__(self, etype, evalue, etraceback):
91    """On exit, re-enters previously saved current directory and cleans up."""
92    os.chdir(self._save_dir)
93    # TODO: detect divergences or shutil.rmtree(self._results_dir)
94
95  def Run(self):
96    """Feeds JFuzz programs into DexFuzz testing."""
97    print()
98    print('**\n**** J/DexFuzz Testing\n**')
99    print()
100    print('#Tests    :', self._num_tests)
101    print('Device    :', self._device)
102    print('Directory :', self._results_dir)
103    print()
104    self.GenerateJFuzzPrograms()
105    self.RunDexFuzz()
106
107  def CompileOnHost(self):
108    """Compiles Test.java into classes.dex using either javac/dx or d8.
109
110    Raises:
111      FatalError: error when compilation fails
112    """
113    if self._dexer == 'dx' or self._dexer == 'd8':
114      dbg = '-g' if self._debug_info else '-g:none'
115      if RunCommand(['javac', '--release=8', dbg, 'Test.java'],
116                    out=None, err='jerr.txt', timeout=30) != RetCode.SUCCESS:
117        print('Unexpected error while running javac')
118        raise FatalError('Unexpected error while running javac')
119      cfiles = glob('*.class')
120      dx = 'dx' if self._dexer == 'dx' else 'd8-compat-dx'
121      if RunCommand([dx, '--dex', '--output=classes.dex'] + cfiles,
122                    out=None, err='dxerr.txt', timeout=30) != RetCode.SUCCESS:
123        print('Unexpected error while running dx')
124        raise FatalError('Unexpected error while running dx')
125      # Cleanup on success (nothing to see).
126      for cfile in cfiles:
127        os.unlink(cfile)
128      os.unlink('jerr.txt')
129      os.unlink('dxerr.txt')
130    else:
131      raise FatalError('Unknown dexer: ' + self._dexer)
132
133  def GenerateJFuzzPrograms(self):
134    """Generates JFuzz programs.
135
136    Raises:
137      FatalError: error when generation fails
138    """
139    os.chdir(self._inputs_dir)
140    for i in range(1, self._num_inputs + 1):
141      if RunCommand(['jfuzz'], out='Test.java', err=None) != RetCode.SUCCESS:
142        print('Unexpected error while running JFuzz')
143        raise FatalError('Unexpected error while running JFuzz')
144      self.CompileOnHost()
145      shutil.move('Test.java', '../Test' + str(i) + '.java')
146      shutil.move('classes.dex', 'classes' + str(i) + '.dex')
147
148  def RunDexFuzz(self):
149    """Starts the DexFuzz testing."""
150    os.chdir(self._dexfuzz_dir)
151    dexfuzz_args = ['--inputs=' + self._inputs_dir,
152                    '--execute',
153                    '--execute-class=Test',
154                    '--repeat=' + str(self._num_tests),
155                    '--quiet',
156                    '--dump-output',
157                    '--dump-verify',
158                    '--interpreter',
159                    '--optimizing',
160                    '--bisection-search']
161    if self._device is not None:
162      dexfuzz_args += ['--device=' + self._device, '--allarm']
163    else:
164      dexfuzz_args += ['--host']  # Assume host otherwise.
165    cmd = ['dexfuzz'] + dexfuzz_args
166    print('**** Running ****\n\n', cmd, '\n')
167    call(cmd, env=self._dexfuzz_env)
168    print('\n**** Results (report.log) ****\n')
169    call(['tail', '-n 24', 'report.log'])
170
171
172def main():
173  # Handle arguments.
174  parser = argparse.ArgumentParser()
175  parser.add_argument('--num_tests', default=1000, type=int,
176                      help='number of tests to run (default: 1000)')
177  parser.add_argument('--num_inputs', default=10, type=int,
178                      help='number of JFuzz program to generate (default: 10)')
179  parser.add_argument('--device', help='target device serial number')
180  parser.add_argument('--dexer', default='dx', type=str,
181                      help='defines dexer as dx or d8 (default: dx)')
182  parser.add_argument('--debug_info', default=False, action='store_true',
183                      help='include debugging info')
184  args = parser.parse_args()
185  # Run the DexFuzz tester.
186  with DexFuzzTester(args.num_tests,
187                     args.num_inputs,
188                     args.device,
189                     args.dexer, args.debug_info) as fuzzer:
190    fuzzer.Run()
191
192if __name__ == '__main__':
193  main()
194