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