1 /*
2  * Copyright (C) 2014 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package dexfuzz.fuzzers;
18 
19 import dexfuzz.Log;
20 import dexfuzz.Options;
21 import dexfuzz.Timer;
22 import dexfuzz.executors.Architecture;
23 import dexfuzz.executors.Arm64InterpreterExecutor;
24 import dexfuzz.executors.Arm64OptimizingBackendExecutor;
25 import dexfuzz.executors.ArmInterpreterExecutor;
26 import dexfuzz.executors.ArmOptimizingBackendExecutor;
27 import dexfuzz.executors.Device;
28 import dexfuzz.executors.Executor;
29 import dexfuzz.executors.X86InterpreterExecutor;
30 import dexfuzz.executors.X86OptimizingBackendExecutor;
31 import dexfuzz.executors.X86_64InterpreterExecutor;
32 import dexfuzz.executors.X86_64OptimizingBackendExecutor;
33 import dexfuzz.listeners.BaseListener;
34 import dexfuzz.program.Mutation;
35 import dexfuzz.program.Program;
36 import dexfuzz.rawdex.DexRandomAccessFile;
37 import dexfuzz.rawdex.OffsetTracker;
38 import dexfuzz.rawdex.RawDexFile;
39 
40 import java.io.FileNotFoundException;
41 import java.io.IOException;
42 import java.lang.reflect.Constructor;
43 import java.lang.reflect.InvocationTargetException;
44 import java.util.ArrayList;
45 import java.util.HashMap;
46 import java.util.List;
47 import java.util.Map;
48 
49 /**
50  * A particular fuzzing strategy, this class provides the common methods
51  * most fuzzing will involve, and subclasses override the run() method, to
52  * employ a particular strategy.
53  */
54 public abstract class Fuzzer {
55   private List<Executor> executors;
56   private OffsetTracker offsetTracker;
57 
58   /**
59    * This is the executor that we use to test for self-divergent programs.
60    */
61   private Executor goldenExecutor;
62 
63   /*
64    * These two flags are set during fuzz(), and then cleared at the end of execute().
65    */
66   private boolean mutatedSuccessfully;
67   private boolean savedSuccessfully;
68 
69   private Timer totalTimer = new Timer("Total Time");
70   private Timer timerDexInput = new Timer("DEX Input");
71   private Timer timerProgGen = new Timer("Program Generation");
72   private Timer timerMutation = new Timer("Mutation Time");
73   private Timer timerDexOutput = new Timer("DEX Output");
74   private Timer timerChecksumCalc = new Timer("Checksum Calculation");
75 
76   protected BaseListener listener;
77 
Fuzzer(BaseListener listener)78   protected Fuzzer(BaseListener listener) {
79     totalTimer.start();
80     executors = new ArrayList<Executor>();
81     this.listener = listener;
82   }
83 
run()84   public abstract void run();
85 
getNextInputFilename()86   protected abstract String getNextInputFilename();
87 
getNextOutputFilename()88   protected abstract String getNextOutputFilename();
89 
90   /**
91    * Call this after fuzzer execution to print out timing results.
92    */
printTimingInfo()93   public void printTimingInfo() {
94     totalTimer.stop();
95     timerDexInput.printTime(listener);
96     timerProgGen.printTime(listener);
97     timerMutation.printTime(listener);
98     timerDexOutput.printTime(listener);
99     timerChecksumCalc.printTime(listener);
100     totalTimer.printTime(listener);
101   }
102 
103   /**
104    * Make sure this is called to correctly shutdown each Executor's StreamConsumers.
105    */
shutdown()106   public void shutdown() {
107     if (executors != null) {
108       for (Executor executor : executors) {
109         executor.shutdown();
110       }
111     }
112   }
113 
addExecutorsForArchitecture(Device device, Class<? extends Executor> optimizing, Class<? extends Executor> interpreter)114   private void addExecutorsForArchitecture(Device device, Class<? extends Executor> optimizing,
115       Class<? extends Executor> interpreter) {
116     // NB: Currently OptimizingBackend MUST come immediately before same arch's Interpreter.
117     // This is because intepreter execution relies on there being an OAT file already
118     // created to produce correct debug information. Otherwise we will see
119     // false-positive divergences.
120     try {
121       if (Options.useOptimizing) {
122         Constructor<? extends Executor> constructor =
123             optimizing.getConstructor(BaseListener.class, Device.class);
124         executors.add(constructor.newInstance(listener, device));
125       }
126       if (Options.useInterpreter) {
127         Constructor<? extends Executor> constructor =
128             interpreter.getConstructor(BaseListener.class, Device.class);
129         executors.add(constructor.newInstance(listener, device));
130       }
131     } catch (NoSuchMethodException e) {
132       Log.errorAndQuit("Executor doesn't have correct constructor.");
133     } catch (InstantiationException e) {
134       Log.errorAndQuit("Executor couldn't be instantiated.");
135     } catch (IllegalAccessException e) {
136       Log.errorAndQuit("Executor couldn't be accessed.");
137     } catch (IllegalArgumentException e) {
138       Log.errorAndQuit("Invalid arguments to instantiation of Executor.");
139     } catch (InvocationTargetException e) {
140       Log.errorAndQuit("Instantiation of Executor threw an Exception!");
141     }
142   }
143 
addExecutors()144   protected void addExecutors() {
145     Device device = null;
146     if (Options.executeOnHost) {
147       device = new Device();
148     } else {
149       device = new Device(Options.deviceName, Options.noBootImage);
150     }
151 
152     if (Options.useArchArm64) {
153       addExecutorsForArchitecture(device, Arm64OptimizingBackendExecutor.class,
154           Arm64InterpreterExecutor.class);
155     }
156 
157     if (Options.useArchArm) {
158       addExecutorsForArchitecture(device, ArmOptimizingBackendExecutor.class,
159           ArmInterpreterExecutor.class);
160     }
161 
162     if (Options.useArchX86_64) {
163       addExecutorsForArchitecture(device, X86_64OptimizingBackendExecutor.class,
164           X86_64InterpreterExecutor.class);
165     }
166 
167     if (Options.useArchX86) {
168       addExecutorsForArchitecture(device, X86OptimizingBackendExecutor.class,
169           X86InterpreterExecutor.class);
170     }
171 
172     // Add the first backend as the golden executor for self-divergence tests.
173     goldenExecutor = executors.get(0);
174   }
175 
176   /**
177    * Called from each Fuzzer subclass that we can instantiate. Parses the program, fuzzes it,
178    * and then saves it, if mutation was successful. We can use --skip-mutation to bypass
179    * the mutation phase, if we wanted to verify that a test program itself works.
180    */
fuzz()181   protected Program fuzz() {
182     String inputFile = getNextInputFilename();
183     Program program = loadProgram(inputFile, null);
184     if (program == null) {
185       Log.errorAndQuit("Problem loading seed file.");
186     }
187     // Mutate the program.
188     if (!Options.skipMutation) {
189       timerMutation.start();
190       program.mutateTheProgram();
191 
192       mutatedSuccessfully = program.updateRawDexFile();
193       timerMutation.stop();
194       if (!mutatedSuccessfully) {
195         listener.handleMutationFail();
196       }
197     } else {
198       Log.info("Skipping mutation stage as requested.");
199       mutatedSuccessfully = true;
200     }
201     if (mutatedSuccessfully) {
202       savedSuccessfully = saveProgram(program, getNextOutputFilename());
203     }
204     return program;
205   }
206 
safeToExecute()207   protected boolean safeToExecute() {
208     return mutatedSuccessfully && savedSuccessfully;
209   }
210 
execute(Program program)211   protected void execute(Program program) {
212     if (!safeToExecute()) {
213       Log.errorAndQuit("Your Fuzzer subclass called execute() "
214           + "without checking safeToExecute()!");
215     }
216 
217     String programName = getNextOutputFilename();
218     boolean verified = true;
219 
220     if (!Options.skipHostVerify && !Options.executeOnHost) {
221       verified = goldenExecutor.verifyOnHost(programName);
222       if (verified) {
223         listener.handleSuccessfulHostVerification();
224       }
225     }
226 
227     if (verified) {
228       boolean skipAnalysis = false;
229 
230       for (Executor executor : executors) {
231         executor.reset();
232         executor.prepareProgramForExecution(programName);
233         executor.execute(programName);
234         if (!executor.didTargetVerify()) {
235           listener.handleFailedTargetVerification();
236           skipAnalysis = true;
237           break;
238         }
239         // Results are saved in the executors until they reset, usually at the
240         // next iteration.
241       }
242 
243       if (!skipAnalysis) {
244         listener.handleSuccessfullyFuzzedFile(programName);
245         analyseResults(program, programName);
246       }
247     }
248 
249     goldenExecutor.finishedWithProgramOnDevice();
250     mutatedSuccessfully = false;
251     savedSuccessfully = false;
252   }
253 
254   /**
255    * Checks if the different outputs we observed align with different architectures.
256    */
checkForArchitectureSplit(Map<String, List<Executor>> outputMap)257   private boolean checkForArchitectureSplit(Map<String, List<Executor>> outputMap) {
258     if (outputMap.size() != 2) {
259       // Cannot have a two-way split if we don't have 2 kinds of output.
260       return false;
261     }
262 
263     Architecture[] architectures = new Architecture[2];
264     int archIdx = 0;
265 
266     // For each kind of output we saw, make sure they all
267     // came from the same architecture.
268     for (List<Executor> executorList : outputMap.values()) {
269       architectures[archIdx] = executorList.get(0).getArchitecture();
270       for (int execIdx = 1; execIdx < executorList.size(); execIdx++) {
271         if (executorList.get(execIdx).getArchitecture() != architectures[archIdx]) {
272           // Not every executor with this output shared the same architecture.
273           return false;
274         }
275       }
276       archIdx++;
277     }
278 
279     // Now make sure that the two outputs we saw were different architectures.
280     if (architectures[0] == architectures[1]) {
281       return false;
282     }
283     return true;
284   }
285 
checkGoldenExecutorForSelfDivergence(String programName)286   private boolean checkGoldenExecutorForSelfDivergence(String programName) {
287     // Run golden executor multiple times, make sure it always produces
288     // the same output, otherwise report that it is self-divergent.
289 
290     // TODO: Instead, produce a list of acceptable outputs, and see if the divergent
291     // outputs of the backends fall within this set of outputs.
292     String seenOutput = null;
293     for (int i = 0; i < Options.divergenceRetry + 1; i++) {
294       goldenExecutor.reset();
295       goldenExecutor.execute(programName);
296       String output = goldenExecutor.getResult().getFlattenedOutput();
297       if (seenOutput == null) {
298         seenOutput = output;
299       } else if (!seenOutput.equals(output)) {
300         return true;
301       }
302     }
303     return false;
304   }
305 
analyseResults(Program program, String programName)306   private void analyseResults(Program program, String programName) {
307     // Check timeouts.
308     // Construct two lists of executors, those who timed out, and those who did not.
309     // Report if we had some timeouts.
310     List<Executor> timedOut = new ArrayList<Executor>();
311     List<Executor> didNotTimeOut = new ArrayList<Executor>();
312     for (Executor executor : executors) {
313       if (executor.getResult().isTimeout()) {
314         timedOut.add(executor);
315       } else {
316         didNotTimeOut.add(executor);
317       }
318     }
319     if (!timedOut.isEmpty()) {
320       listener.handleTimeouts(timedOut, didNotTimeOut);
321       // Do not bother reporting divergence information.
322       return;
323     }
324 
325     // Check divergences.
326     // Construct a map {output1: [executor that produced output1, ...], output2: [...]}
327     // If the map has more than one output, we had divergence, report it.
328     Map<String, List<Executor>> outputMap = new HashMap<String, List<Executor>>();
329     for (Executor executor : executors) {
330       String output = executor.getResult().getFlattenedOutput();
331       if (Options.dumpOutput) {
332         listener.handleDumpOutput(
333             executor.getResult().getFlattenedOutputWithNewlines(), executor);
334       }
335       if (outputMap.containsKey(output)) {
336         outputMap.get(output).add(executor);
337       } else {
338         List<Executor> newList = new ArrayList<Executor>();
339         newList.add(executor);
340         outputMap.put(output, newList);
341       }
342     }
343 
344     if (outputMap.size() > 1) {
345       // Report that we had divergence.
346       listener.handleDivergences(outputMap);
347       listener.handleMutations(program.getMutations());
348       // If we found divergences, try running the "golden executor"
349       // a few times in succession, to see if the output it produces is different
350       // from run to run. If so, then we're probably executing something with either:
351       //  a) randomness
352       //  b) timing-dependent code
353       //  c) threads
354       // So we will not consider it a "true" divergence, but still useful?
355       if (checkGoldenExecutorForSelfDivergence(programName)) {
356         listener.handleSelfDivergence();
357         return;
358       }
359       // If we found divergences, try checking if the differences
360       // in outputs align with differences in architectures.
361       // For example, if we have: {Output1: [ARM, ARM], Output2: [ARM64, ARM64]}
362       if (checkForArchitectureSplit(outputMap)) {
363         listener.handleArchitectureSplit();
364       }
365     } else {
366       // No problems with execution.
367       listener.handleSuccess(outputMap);
368     }
369   }
370 
loadProgram(String inputName, List<Mutation> mutations)371   private Program loadProgram(String inputName, List<Mutation> mutations) {
372     Program program = null;
373     try {
374       DexRandomAccessFile input = new DexRandomAccessFile(inputName, "r");
375       offsetTracker = new OffsetTracker();
376       input.setOffsetTracker(offsetTracker);
377       // Read the raw DexFile
378       RawDexFile rawDexFile = new RawDexFile();
379       timerDexInput.start();
380       rawDexFile.read(input);
381       timerDexInput.stop();
382       input.close();
383       // Create the program view.
384       timerProgGen.start();
385       program = new Program(rawDexFile, mutations, listener);
386       timerProgGen.stop();
387     } catch (FileNotFoundException e) {
388       Log.errorAndQuit("Couldn't open a file called " + inputName);
389     } catch (IOException e) {
390       Log.errorAndQuit("IOException when trying to load a DEX test file!");
391     }
392     return program;
393   }
394 
saveProgram(Program program, String outputName)395   private boolean saveProgram(Program program, String outputName) {
396     boolean success = false;
397 
398     try {
399       // Write out the results of mutation.
400       DexRandomAccessFile output = new DexRandomAccessFile(outputName, "rw");
401       output.setOffsetTracker(offsetTracker);
402       // Delete the contents of the file, in case it already existed.
403       output.setLength(0);
404       // Write out the file.
405       timerDexOutput.start();
406       program.writeRawDexFile(output);
407       timerDexOutput.stop();
408       // Recalculate the header info.
409       timerChecksumCalc.start();
410       program.updateRawDexFileHeader(output);
411       timerChecksumCalc.stop();
412       output.close();
413       success = true;
414     } catch (FileNotFoundException e) {
415       Log.errorAndQuit("Couldn't open a file called " + outputName);
416     } catch (IOException e) {
417       Log.errorAndQuit("IOException when trying to save a DEX test file!");
418     }
419     return success;
420   }
421 }
422