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