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;
18 
19 import dexfuzz.Log.LogTag;
20 
21 import java.io.BufferedReader;
22 import java.io.File;
23 import java.io.FileNotFoundException;
24 import java.io.FileReader;
25 import java.io.IOException;
26 import java.util.ArrayList;
27 import java.util.HashMap;
28 import java.util.List;
29 import java.util.Map;
30 
31 /**
32  * Stores options for dexfuzz.
33  */
34 public class Options {
35   /**
36    * Constructor has been disabled for this class, which should only be used statically.
37    */
Options()38   private Options() { }
39 
40   // KEY VALUE OPTIONS
41   public static final List<String> inputFileList = new ArrayList<String>();
42   public static String outputFile = "";
43   public static long rngSeed = -1;
44   public static boolean usingProvidedSeed = false;
45   public static int methodMutations = 3;
46   public static int minMethods = 2;
47   public static int maxMethods = 10;
48   public static final Map<String,Integer> mutationLikelihoods = new HashMap<String,Integer>();
49   public static String executeClass = "Main";
50   public static String deviceName = "";
51   public static boolean usingSpecificDevice = false;
52   public static int repeat = 1;
53   public static int divergenceRetry = 10;
54   public static String executeDirectory = "/data/art-test";
55   public static String androidRoot = "";
56   public static String dumpMutationsFile = "mutations.dump";
57   public static String loadMutationsFile = "mutations.dump";
58   public static String reportLogFile = "report.log";
59   public static String uniqueDatabaseFile = "unique_progs.db";
60 
61   // FLAG OPTIONS
62   public static boolean execute;
63   public static boolean executeOnHost;
64   public static boolean noBootImage;
65   public static boolean useInterpreter;
66   public static boolean useOptimizing;
67   public static boolean useArchArm;
68   public static boolean useArchArm64;
69   public static boolean useArchX86;
70   public static boolean useArchX86_64;
71   public static boolean useArchMips;
72   public static boolean useArchMips64;
73   public static boolean skipHostVerify;
74   public static boolean shortTimeouts;
75   public static boolean dumpOutput;
76   public static boolean dumpVerify;
77   public static boolean mutateLimit;
78   public static boolean reportUnique;
79   public static boolean skipMutation;
80   public static boolean dumpMutations;
81   public static boolean loadMutations;
82   public static boolean runBisectionSearch;
83   public static boolean quiet;
84 
85   /**
86    * Print out usage information about dexfuzz, and then exit.
87    */
usage()88   public static void usage() {
89     Log.always("DexFuzz Usage:");
90     Log.always("  --input=<file>         : Seed DEX file to be fuzzed");
91     Log.always("                           (Can specify multiple times.)");
92     Log.always("  --inputs=<file>        : Directory containing DEX files to be fuzzed.");
93     Log.always("  --output=<file>        : Output DEX file to be produced");
94     Log.always("");
95     Log.always("  --execute              : Execute the resulting fuzzed program");
96     Log.always("    --host               : Execute on host");
97     Log.always("    --device=<device>    : Execute on an ADB-connected-device, where <device> is");
98     Log.always("                           the argument given to adb -s. Default execution mode.");
99     Log.always("    --execute-dir=<dir>  : Push tests to this directory to execute them.");
100     Log.always("                           (Default: /data/art-test)");
101     Log.always("    --android-root=<dir> : Set path where dalvikvm should look for binaries.");
102     Log.always("                           Use this when pushing binaries to a custom location.");
103     Log.always("    --no-boot-image      : Use this flag when boot.art is not available.");
104     Log.always("    --skip-host-verify   : When executing, skip host-verification stage");
105     Log.always("    --execute-class=<c>  : When executing, execute this class (default: Main)");
106     Log.always("");
107     Log.always("    --interpreter        : Include the Interpreter in comparisons");
108     Log.always("    --optimizing         : Include the Optimizing Compiler in comparisons");
109     Log.always("");
110     Log.always("    --arm                : Include ARM backends in comparisons");
111     Log.always("    --arm64              : Include ARM64 backends in comparisons");
112     Log.always("    --allarm             : Short for --arm --arm64");
113     Log.always("    --x86                : Include x86 backends in comparisons");
114     Log.always("    --x86-64             : Include x86-64 backends in comparisons");
115     Log.always("    --mips               : Include MIPS backends in comparisons");
116     Log.always("    --mips64             : Include MIPS64 backends in comparisons");
117     Log.always("");
118     Log.always("    --dump-output        : Dump outputs of executed programs");
119     Log.always("    --dump-verify        : Dump outputs of verification");
120     Log.always("    --repeat=<n>         : Fuzz N programs, executing each one.");
121     Log.always("    --short-timeouts     : Shorten timeouts (faster; use if");
122     Log.always("                           you want to focus on output divergences)");
123     Log.always("    --divergence-retry=<n> : Number of retries when checking if test is");
124     Log.always("                           self-divergent. (Default: 10)");
125     Log.always("  --seed=<seed>          : RNG seed to use");
126     Log.always("  --method-mutations=<n> : Maximum number of mutations to perform on each method.");
127     Log.always("                           (Default: 3)");
128     Log.always("  --min-methods=<n>      : Minimum number of methods to mutate. (Default: 2)");
129     Log.always("  --max-methods=<n>      : Maximum number of methods to mutate. (Default: 10)");
130     Log.always("  --one-mutation         : Short for --method-mutations=1 ");
131     Log.always("                             --min-methods=1 --max-methods=1");
132     Log.always("  --likelihoods=<file>   : A file containing a table of mutation likelihoods");
133     Log.always("  --mutate-limit         : Mutate only methods whose names end with _MUTATE");
134     Log.always("  --skip-mutation        : Do not actually mutate the input, just output it");
135     Log.always("                           after parsing");
136     Log.always("");
137     Log.always("  --dump-mutations[=<file>] : Dump an editable set of mutations applied");
138     Log.always("                              to <file> (default: mutations.dump)");
139     Log.always("  --load-mutations[=<file>] : Load and apply a set of mutations");
140     Log.always("                              from <file> (default: mutations.dump)");
141     Log.always("  --log=<tag>            : Set more verbose logging level: DEBUG, INFO, WARN");
142     Log.always("  --report=<file>        : Use <file> to report results when using --repeat");
143     Log.always("                           (Default: report.log)");
144     Log.always("  --report-unique        : Print out information about unique programs generated");
145     Log.always("  --unique-db=<file>     : Use <file> store results about unique programs");
146     Log.always("                           (Default: unique_progs.db)");
147     Log.always("  --bisection-search     : Run bisection search for divergences");
148     Log.always("  --quiet                : Disables progress log");
149     Log.always("");
150     System.exit(0);
151   }
152 
153   /**
154    * Given a flag option (one that does not feature an =), handle it
155    * accordingly. Report an error and print usage info if the flag is not
156    * recognised.
157    */
handleFlagOption(String flag)158   private static void handleFlagOption(String flag) {
159     if (flag.equals("execute")) {
160       execute = true;
161     } else if (flag.equals("host")) {
162       executeOnHost = true;
163     } else if (flag.equals("no-boot-image")) {
164       noBootImage = true;
165     } else if (flag.equals("skip-host-verify")) {
166       skipHostVerify = true;
167     } else if (flag.equals("interpreter")) {
168       useInterpreter = true;
169     } else if (flag.equals("optimizing")) {
170       useOptimizing = true;
171     } else if (flag.equals("arm")) {
172       useArchArm = true;
173     } else if (flag.equals("arm64")) {
174       useArchArm64 = true;
175     } else if (flag.equals("allarm")) {
176       useArchArm = true;
177       useArchArm64 = true;
178     } else if (flag.equals("x86")) {
179       useArchX86 = true;
180     } else if (flag.equals("x86-64")) {
181       useArchX86_64 = true;
182     } else if (flag.equals("mips")) {
183       useArchMips = true;
184     } else if (flag.equals("mips64")) {
185       useArchMips64 = true;
186     } else if (flag.equals("mutate-limit")) {
187       mutateLimit = true;
188     } else if (flag.equals("report-unique")) {
189       reportUnique = true;
190     } else if (flag.equals("dump-output")) {
191       dumpOutput = true;
192     } else if (flag.equals("dump-verify")) {
193       dumpVerify = true;
194     } else if (flag.equals("short-timeouts")) {
195       shortTimeouts = true;
196     } else if (flag.equals("skip-mutation")) {
197       skipMutation = true;
198     } else if (flag.equals("dump-mutations")) {
199       dumpMutations = true;
200     } else if (flag.equals("load-mutations")) {
201       loadMutations = true;
202     } else if (flag.equals("one-mutation")) {
203       methodMutations = 1;
204       minMethods = 1;
205       maxMethods = 1;
206     } else if (flag.equals("bisection-search")) {
207       runBisectionSearch = true;
208     } else if (flag.equals("quiet")) {
209       quiet = true;
210     } else if (flag.equals("help")) {
211       usage();
212     } else {
213       Log.error("Unrecognised flag: --" + flag);
214       usage();
215     }
216   }
217 
218   /**
219    * Given a key-value option (one that features an =), handle it
220    * accordingly. Report an error and print usage info if the key is not
221    * recognised.
222    */
handleKeyValueOption(String key, String value)223   private static void handleKeyValueOption(String key, String value) {
224     if (key.equals("input")) {
225       inputFileList.add(value);
226     } else if (key.equals("inputs")) {
227       File folder = new File(value);
228       if (folder.listFiles() == null) {
229         Log.errorAndQuit("Specified argument to --inputs is not a directory!");
230       }
231       for (File file : folder.listFiles()) {
232         String inputName = value + "/" + file.getName();
233         Log.always("Adding " + inputName + " to input seed files.");
234         inputFileList.add(inputName);
235       }
236     } else if (key.equals("output")) {
237       outputFile = value;
238     } else if (key.equals("seed")) {
239       rngSeed = Long.parseLong(value);
240       usingProvidedSeed = true;
241     } else if (key.equals("method-mutations")) {
242       methodMutations = Integer.parseInt(value);
243     } else if (key.equals("min-methods")) {
244       minMethods = Integer.parseInt(value);
245     } else if (key.equals("max-methods")) {
246       maxMethods = Integer.parseInt(value);
247     } else if (key.equals("repeat")) {
248       repeat = Integer.parseInt(value);
249     } else if (key.equals("divergence-retry")) {
250       divergenceRetry = Integer.parseInt(value);
251     } else if (key.equals("log")) {
252       Log.setLoggingLevel(LogTag.valueOf(value.toUpperCase()));
253     } else if (key.equals("likelihoods")) {
254       setupMutationLikelihoodTable(value);
255     } else if (key.equals("dump-mutations")) {
256       dumpMutations = true;
257       dumpMutationsFile = value;
258     } else if (key.equals("load-mutations")) {
259       loadMutations = true;
260       loadMutationsFile = value;
261     } else if (key.equals("report")) {
262       reportLogFile = value;
263     } else if (key.equals("unique-db")) {
264       uniqueDatabaseFile = value;
265     } else if (key.equals("execute-class")) {
266       executeClass = value;
267     } else if (key.equals("device")) {
268       deviceName = value;
269       usingSpecificDevice = true;
270     } else if (key.equals("execute-dir")) {
271       executeDirectory = value;
272     } else if (key.equals("android-root")) {
273       androidRoot = value;
274     } else {
275       Log.error("Unrecognised key: --" + key);
276       usage();
277     }
278   }
279 
setupMutationLikelihoodTable(String tableFilename)280   private static void setupMutationLikelihoodTable(String tableFilename) {
281     try {
282       BufferedReader reader = new BufferedReader(new FileReader(tableFilename));
283       String line = reader.readLine();
284       while (line != null) {
285         line = line.replaceAll("\\s+", " ");
286         String[] entries = line.split(" ");
287         String name = entries[0].toLowerCase();
288         int likelihood = Integer.parseInt(entries[1]);
289         if (likelihood > 100) {
290           likelihood = 100;
291         }
292         if (likelihood < 0) {
293           likelihood = 0;
294         }
295         mutationLikelihoods.put(name, likelihood);
296         line = reader.readLine();
297       }
298       reader.close();
299     } catch (FileNotFoundException e) {
300       Log.error("Unable to open mutation probability table file: " + tableFilename);
301     } catch (IOException e) {
302       Log.error("Unable to read mutation probability table file: " + tableFilename);
303     }
304   }
305 
306   /**
307    * Called by the DexFuzz class during program initialisation to parse
308    * the program's command line arguments.
309    * @return If options were successfully read and validated.
310    */
readOptions(String[] args)311   public static boolean readOptions(String[] args) {
312     for (String arg : args) {
313       if (!(arg.startsWith("--"))) {
314         Log.error("Unrecognised option: " + arg);
315         usage();
316       }
317 
318       // cut off the --
319       arg = arg.substring(2);
320 
321       // choose between a --X=Y option (keyvalue) and a --X option (flag)
322       if (arg.contains("=")) {
323         String[] split = arg.split("=");
324         handleKeyValueOption(split[0], split[1]);
325       } else {
326         handleFlagOption(arg);
327       }
328     }
329 
330     return validateOptions();
331   }
332 
333   /**
334    * Checks if the current options settings are valid, called after reading
335    * all options.
336    * @return If the options are valid or not.
337    */
validateOptions()338   private static boolean validateOptions() {
339     // Deal with option assumptions.
340     if (inputFileList.isEmpty()) {
341       File seedFile = new File("fuzzingseed.dex");
342       if (seedFile.exists()) {
343         Log.always("Assuming --input=fuzzingseed.dex");
344         inputFileList.add("fuzzingseed.dex");
345       } else {
346         Log.errorAndQuit("No input given, and couldn't find fuzzingseed.dex!");
347         return false;
348       }
349     }
350 
351     if (outputFile.equals("")) {
352       Log.always("Assuming --output=fuzzingseed_fuzzed.dex");
353       outputFile = "fuzzingseed_fuzzed.dex";
354     }
355 
356 
357     if (mutationLikelihoods.isEmpty()) {
358       File likelihoodsFile = new File("likelihoods.txt");
359       if (likelihoodsFile.exists()) {
360         Log.always("Assuming --likelihoods=likelihoods.txt ");
361         setupMutationLikelihoodTable("likelihoods.txt");
362       } else {
363         Log.always("Using default likelihoods (see README for values)");
364       }
365     }
366 
367     // Now check for hard failures.
368     if (repeat < 1) {
369       Log.error("--repeat must be at least 1!");
370       return false;
371     }
372     if (divergenceRetry < 0) {
373       Log.error("--divergence-retry cannot be negative!");
374       return false;
375     }
376     if (usingProvidedSeed && repeat > 1) {
377       Log.error("Cannot use --repeat with --seed");
378       return false;
379     }
380     if (loadMutations && dumpMutations) {
381       Log.error("Cannot both load and dump mutations");
382       return false;
383     }
384     if (repeat == 1 && inputFileList.size() > 1) {
385       Log.error("Must use --repeat if you have provided more than one input");
386       return false;
387     }
388     if (methodMutations < 0) {
389       Log.error("Cannot use --method-mutations with a negative value.");
390       return false;
391     }
392     if (minMethods < 0) {
393       Log.error("Cannot use --min-methods with a negative value.");
394       return false;
395     }
396     if (maxMethods < 0) {
397       Log.error("Cannot use --max-methods with a negative value.");
398       return false;
399     }
400     if (maxMethods < minMethods) {
401       Log.error("Cannot use --max-methods that's smaller than --min-methods");
402       return false;
403     }
404     if (executeOnHost && usingSpecificDevice) {
405       Log.error("Cannot use --host and --device!");
406       return false;
407     }
408     if (execute) {
409       // When host-execution mode is specified, we don't need to select an architecture.
410       if (!executeOnHost) {
411         if (!(useArchArm
412             || useArchArm64
413             || useArchX86
414             || useArchX86_64
415             || useArchMips
416             || useArchMips64)) {
417           Log.error("No architecture to execute on was specified!");
418           return false;
419         }
420       } else {
421         // TODO: Select the correct architecture. For now, just assume x86.
422         useArchX86 = true;
423       }
424       if ((useArchArm || useArchArm64) && (useArchX86 || useArchX86_64)) {
425         Log.error("Did you mean to specify ARM and x86?");
426         return false;
427       }
428       if ((useArchArm || useArchArm64) && (useArchMips || useArchMips64)) {
429         Log.error("Did you mean to specify ARM and MIPS?");
430         return false;
431       }
432       if ((useArchX86 || useArchX86_64) && (useArchMips || useArchMips64)) {
433         Log.error("Did you mean to specify x86 and MIPS?");
434         return false;
435       }
436       int backends = 0;
437       if (useInterpreter) {
438         backends++;
439       }
440       if (useOptimizing) {
441         backends++;
442       }
443       if (useArchArm && useArchArm64) {
444         // Could just be comparing optimizing-ARM versus optimizing-ARM64?
445         backends++;
446       }
447       if (backends < 2) {
448         Log.error("Not enough backends specified! Try --optimizing --interpreter!");
449         return false;
450       }
451     }
452 
453     return true;
454   }
455 }
456