Home | History | Annotate | Download | only in fuzzers
      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 
     82   protected Fuzzer(BaseListener listener) {
     83     totalTimer.start();
     84     executors = new ArrayList<Executor>();
     85     this.listener = listener;
     86   }
     87 
     88   public abstract void run();
     89 
     90   protected abstract String getNextInputFilename();
     91 
     92   protected abstract String getNextOutputFilename();
     93 
     94   /**
     95    * Call this after fuzzer execution to print out timing results.
     96    */
     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    */
    110   public void shutdown() {
    111     if (executors != null) {
    112       for (Executor executor : executors) {
    113         executor.shutdown();
    114       }
    115     }
    116   }
    117 
    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 
    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    */
    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 
    221   protected boolean safeToExecute() {
    222     return mutatedSuccessfully && savedSuccessfully;
    223   }
    224 
    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    */
    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 
    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 
    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 
    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 
    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