All Downloads are FREE. Search and download functionalities are using the official Maven repository.

edu.berkeley.cs.jqf.fuzz.ei.ExecutionIndexingGuidance Maven / Gradle / Ivy

There is a newer version: 2.0
Show newest version
/*
 * Copyright (c) 2017-2018 The Regents of the University of California
 *
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are
 * met:
 *
 * 1. Redistributions of source code must retain the above copyright
 * notice, this list of conditions and the following disclaimer.
 *
 * 2. Redistributions in binary form must reproduce the above copyright
 * notice, this list of conditions and the following disclaimer in the
 * documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package edu.berkeley.cs.jqf.fuzz.ei;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.Console;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.time.Duration;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.Deque;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;

import edu.berkeley.cs.jqf.fuzz.ei.ExecutionIndex.Prefix;
import edu.berkeley.cs.jqf.fuzz.ei.ExecutionIndex.Suffix;
import edu.berkeley.cs.jqf.fuzz.guidance.Guidance;
import edu.berkeley.cs.jqf.fuzz.guidance.GuidanceException;
import edu.berkeley.cs.jqf.fuzz.guidance.Result;
import edu.berkeley.cs.jqf.fuzz.guidance.TimeoutException;
import edu.berkeley.cs.jqf.fuzz.util.Coverage;
import edu.berkeley.cs.jqf.fuzz.util.ProducerHashMap;
import edu.berkeley.cs.jqf.instrument.tracing.events.CallEvent;
import edu.berkeley.cs.jqf.instrument.tracing.events.ReturnEvent;
import edu.berkeley.cs.jqf.instrument.tracing.events.TraceEvent;
import edu.berkeley.cs.jqf.instrument.tracing.events.TraceEventVisitor;

import static java.lang.Math.ceil;
import static java.lang.Math.log;

/**
 * A guidance that performs coverage-guided fuzz testing, where inputs
 * are represented not as sequences of bytes but instead as maps of
 * execution indexes to bytes.
 *
 * 

Whenever the input generator for a test requests a new byte, the * execution index of that event is used to query a value in the input * map. This representation retains much more structure of the input * than a simple linear sequence.

* * @author Rohan Padhye */ public class ExecutionIndexingGuidance implements Guidance, TraceEventVisitor { // Currently, we only support single-threaded applications // This field is used to ensure that private Thread appThread; /** The last event handled by this guidance */ private TraceEvent lastEvent; /** The execution indexing logic. */ private ExecutionIndexingState eiState; /** A pseudo-random number generator for generating fresh values. */ private Random random = new Random(); /** The name of the test for display purposes. */ private final String testName; // ------------ ALGORITHM BOOKKEEPING ------------ /** The max amount of time to run for, in milli-seconds */ private final long maxDurationMillis; /** The number of trials completed. */ private long numTrials = 0; /** The number of valid inputs. */ private long numValid = 0; /** The directory where fuzzing results are written. */ private final File outputDirectory; /** The directory where saved inputs are written. */ private File savedInputsDirectory; /** The directory where saved inputs are written. */ private File savedFailuresDirectory; /** Set of saved inputs to fuzz. */ private ArrayList savedInputs = new ArrayList<>(); /** Queue of seeds to fuzz. */ private Deque seedInputs = new ArrayDeque<>(); /** Current input that's running -- valid after getInput() and before handleResult(). */ private Input currentInput; /** Index of currentInput in the savedInputs -- valid after seeds are processed (OK if this is inaccurate). */ private int currentParentInputIdx = 0; /** Number of mutated inputs generated from currentInput. */ private int numChildrenGeneratedForCurrentParentInput = 0; /** Number of cycles completed (i.e. how many times we've reset currentParentInputIdx to 0. */ private int cyclesCompleted = 0; /** Number of favored inputs in the last cycle. */ private int numFavoredLastCycle = 0; /** Number of saved inputs. * * This is usually the same as savedInputs.size(), * but we do not really save inputs in TOTALLY_RANDOM mode. */ private int numSavedInputs = 0; /** Coverage statistics for a single run. */ private Coverage runCoverage = new Coverage(); /** Cumulative coverage statistics. */ private Coverage totalCoverage = new Coverage(); /** Cumulative coverage for valid inputs. */ private Coverage validCoverage = new Coverage(); /** The maximum number of keys covered by any single input found so far. */ private int maxCoverage = 0; /** A mapping of coverage keys to inputs that are responsible for them. */ private Map responsibleInputs = new HashMap<>(totalCoverage.size()); /** The set of unique failures found so far. */ Set> uniqueFailures = new HashSet<>(); /** * A map of execution contexts (call stacks) to locations in saved inputs with those contexts. * * This is a nifty data structure for quickly finding candidates for input splicing. */ private Map> ecToInputLoc = new ProducerHashMap<>(() -> new ArrayList<>()); // ---------- LOGGING / STATS OUTPUT ------------ /** Whether to print log statements to stderr (debug option; manually edit). */ private final boolean verbose = true; /** A system console, which is non-null only if STDOUT is a console. */ private final Console console = System.console(); /** Time since this guidance instance was created. */ private final Date startTime = new Date(); /** Time at last stats refresh. */ private Date lastRefreshTime = startTime; /** Total execs at last stats refresh. */ private long lastNumTrials = 0; /** Minimum amount of time (in millis) between two stats refreshes. */ private static final long STATS_REFRESH_TIME_PERIOD = 300; /** The file where log data is written. */ private File logFile; /** The file where saved plot data is written. */ private File statsFile; /** The currently executing input (for debugging purposes). */ private File currentInputFile; /** Whether to print the fuzz config to the stats screen. */ private static boolean SHOW_CONFIG = false; // ------------- TIMEOUT HANDLING ------------ /** Timeout for an individual run. */ private long singleRunTimeoutMillis; /** Date when last run was started. */ private Date runStart; /** Number of conditional jumps since last run was started. */ private long branchCount; // ------------- FUZZING HEURISTICS ------------ /** Turn this on to disable all guidance (i.e. no mutations, only random fuzzing) */ static final boolean TOTALLY_RANDOM = Boolean.getBoolean("jqf.ei.TOTALLY_RANDOM"); /** Whether to use real execution indexes as opposed to flat numbering. */ static final boolean DISABLE_EXECUTION_INDEXING = !Boolean.getBoolean("jqf.ei.ENABLE_EXECUTION_INDEXING"); /** Whether to save only valid inputs **/ static final boolean SAVE_ONLY_VALID = Boolean.getBoolean("jqf.ei.SAVE_ONLY_VALID"); /** Max input size to generate. */ static final int MAX_INPUT_SIZE = Integer.getInteger("jqf.ei.MAX_INPUT_SIZE", 1024); /** Whether to generate EOFs when we run out of bytes in the input, instead of randomly generating new bytes. **/ static final boolean GENERATE_EOF_WHEN_OUT = Boolean.getBoolean("jqf.ei.GENERATE_EOF_WHEN_OUT"); /** Baseline number of mutated children to produce from a given parent input. */ static final int NUM_CHILDREN_BASELINE = 50; /** Multiplication factor for number of children to produce for favored inputs. */ static final int NUM_CHILDREN_MULTIPLIER_FAVORED = 20; /** Mean number of mutations to perform in each round. */ static final double MEAN_MUTATION_COUNT = 8.0; /** Mean number of contiguous bytes to mutate in each mutation. */ static final double MEAN_MUTATION_SIZE = 4.0; // Bytes /** Max number of contiguous bytes to splice in from another input during the splicing stage. */ static final int MAX_SPLICE_SIZE = 64; // Bytes /** Whether to splice only in the same sub-tree */ static final boolean SPLICE_SUBTREE = Boolean.getBoolean("jqf.ei.SPLICE_SUBTREE"); /** Whether to save inputs that only add new coverage bits (but no new responsibilities). */ static final boolean SAVE_NEW_COUNTS = true; /** Whether to steal responsibility from old inputs (this increases computation cost). */ static final boolean STEAL_RESPONSIBILITY = Boolean.getBoolean("jqf.ei.STEAL_RESPONSIBILITY"); /** Probability of splicing in getOrGenerateFresh() */ static final double DEMAND_DRIVEN_SPLICING_PROBABILITY = 0; /** * @param testName the name of test to display on the status screen * Creates a new execution-index-parametric guidance. * * @param duration the amount of time to run fuzzing for, where * {@code null} indicates unlimited time. * @param outputDirectory the directory where fuzzing results will be written * @throws IOException if the output directory could not be prepared */ public ExecutionIndexingGuidance(String testName, Duration duration, File outputDirectory) throws IOException { this.testName = testName; this.maxDurationMillis = duration != null ? duration.toMillis() : Long.MAX_VALUE; this.outputDirectory = outputDirectory; prepareOutputDirectory(); // Try to parse the single-run timeout String timeout = System.getProperty("jqf.ei.TIMEOUT"); if (timeout != null && !timeout.isEmpty()) { try { // Interpret the timeout as milliseconds (just like `afl-fuzz -t`) this.singleRunTimeoutMillis = Long.parseLong(timeout); } catch (NumberFormatException e1) { throw new IllegalArgumentException("Invalid timeout duration: " + timeout); } } } /** * @param testName the name of test to display on the status screen * @param duration the amount of time to run fuzzing for, where * {@code null} indicates unlimited time. * @param outputDirectory the directory where fuzzing results will be written * @param seedInputFiles one or more input files to be used as initial inputs * @throws IOException if the output directory could not be prepared */ public ExecutionIndexingGuidance(String testName, Duration duration, File outputDirectory, File... seedInputFiles) throws IOException { this(testName, duration, outputDirectory); for (File seedInputFile : seedInputFiles) { seedInputs.add(new SeedInput(seedInputFile)); } } private void prepareOutputDirectory() throws IOException { // Create the output directory if it does not exist if (!outputDirectory.exists()) { if (!outputDirectory.mkdirs()) { throw new IOException("Could not create output directory" + outputDirectory.getAbsolutePath()); } } // Make sure we can write to output directory if (!outputDirectory.isDirectory() || !outputDirectory.canWrite()) { throw new IOException("Output directory is not a writable directory: " + outputDirectory.getAbsolutePath()); } // Name files and directories after AFL this.savedInputsDirectory = new File(outputDirectory, "corpus"); this.savedInputsDirectory.mkdirs(); this.savedFailuresDirectory = new File(outputDirectory, "failures"); this.savedFailuresDirectory.mkdirs(); this.statsFile = new File(outputDirectory, "plot_data"); this.logFile = new File(outputDirectory, "fuzz.log"); this.currentInputFile = new File(outputDirectory, ".cur_input"); // Delete everything that we may have created in a previous run. // Trying to stay away from recursive delete of parent output directory in case there was a // typo and that was not a directory we wanted to nuke. // We also do not check if the deletes are actually successful. statsFile.delete(); logFile.delete(); for (File file : savedInputsDirectory.listFiles()) { file.delete(); } for (File file : savedFailuresDirectory.listFiles()) { file.delete(); } appendLineToFile(statsFile,"# unix_time, cycles_done, cur_path, paths_total, pending_total, " + "pending_favs, map_size, unique_crashes, unique_hangs, max_depth, execs_per_sec, valid_inputs, invalid_inputs, valid_cov"); } private void appendLineToFile(File file, String line) throws GuidanceException { try (PrintWriter out = new PrintWriter(new FileWriter(file, true))) { out.println(line); } catch (IOException e) { throw new GuidanceException(e); } } private void infoLog(String str, Object... args) { if (verbose) { String line = String.format(str, args); if (logFile != null) { appendLineToFile(logFile, line); } else { System.err.println(line); } } } private String millisToDuration(long millis) { long seconds = TimeUnit.MILLISECONDS.toSeconds(millis % TimeUnit.MINUTES.toMillis(1)); long minutes = TimeUnit.MILLISECONDS.toMinutes(millis % TimeUnit.HOURS.toMillis(1)); long hours = TimeUnit.MILLISECONDS.toHours(millis); String result = ""; if (hours > 0) { result = hours + "h "; } if (hours > 0 || minutes > 0) { result += minutes + "m "; } result += seconds + "s"; return result; } // Call only if console exists private void displayStats() { assert (console != null); Date now = new Date(); long intervalMilliseconds = now.getTime() - lastRefreshTime.getTime(); if (intervalMilliseconds < STATS_REFRESH_TIME_PERIOD) { return; } long interlvalTrials = numTrials - lastNumTrials; long intervalExecsPerSec = interlvalTrials * 1000L / intervalMilliseconds; double intervalExecsPerSecDouble = interlvalTrials * 1000.0 / intervalMilliseconds; lastRefreshTime = now; lastNumTrials = numTrials; long elapsedMilliseconds = now.getTime() - startTime.getTime(); long execsPerSec = numTrials * 1000L / elapsedMilliseconds; String currentParentInputDesc; if (seedInputs.size() > 0 || savedInputs.isEmpty()) { currentParentInputDesc = ""; } else { Input currentParentInput = savedInputs.get(currentParentInputIdx); currentParentInputDesc = currentParentInputIdx + " "; currentParentInputDesc += currentParentInput.isFavored() ? "(favored)" : "(not favored)"; currentParentInputDesc += " {" + numChildrenGeneratedForCurrentParentInput + "/" + getTargetChildrenForParent(currentParentInput) + " mutations}"; } int nonZeroCount = totalCoverage.getNonZeroCount(); double nonZeroFraction = nonZeroCount * 100.0 / totalCoverage.size(); int nonZeroValidCount = validCoverage.getNonZeroCount(); double nonZeroValidFraction = nonZeroValidCount * 100.0 / validCoverage.size(); console.printf("\033[2J"); console.printf("\033[H"); console.printf("JQF: Feedback-directed Generator-based Fuzzing\n"); console.printf("----------------------------------------------\n"); if (this.testName != null) { console.printf("Test name: %s\n", this.testName); } console.printf("Results directory: %s\n", this.outputDirectory.getAbsolutePath()); if (SHOW_CONFIG) { if (TOTALLY_RANDOM) { console.printf("Config: TOTALLY_RANDOM\n"); } else { console.printf("Config: DISABLE_EXECUTION_INDEXING = %s,\n" + " STEAL_RESPONSIBILITY = %s,\n" + " SPLICE_SUBTREE = %s\n\n", DISABLE_EXECUTION_INDEXING, STEAL_RESPONSIBILITY, SPLICE_SUBTREE); } } console.printf("Elapsed time: %s (%s)\n", millisToDuration(elapsedMilliseconds), maxDurationMillis == Long.MAX_VALUE ? "no time limit" : ("max " + millisToDuration(maxDurationMillis))); console.printf("Number of executions: %,d\n", numTrials); console.printf("Valid inputs: %,d (%.2f%%)\n", numValid, numValid*100.0/numTrials); console.printf("Cycles completed: %d\n", cyclesCompleted); console.printf("Unique failures: %,d\n", uniqueFailures.size()); console.printf("Queue size: %,d (%,d favored last cycle)\n", savedInputs.size(), numFavoredLastCycle); console.printf("Current parent input: %s\n", currentParentInputDesc); console.printf("Execution speed: %,d/sec now | %,d/sec overall\n", intervalExecsPerSec, execsPerSec); console.printf("Total coverage: %,d (%.2f%% of map)\n", nonZeroCount, nonZeroFraction); console.printf("Valid coverage: %,d (%.2f%% of map)\n", nonZeroValidCount, nonZeroValidFraction); String plotData = String.format("%d, %d, %d, %d, %d, %d, %.2f%%, %d, %d, %d, %.2f, %d, %d, %.2f%%", TimeUnit.MILLISECONDS.toSeconds(now.getTime()), cyclesCompleted, currentParentInputIdx, savedInputs.size(), 0, 0, nonZeroFraction, uniqueFailures.size(), 0, 0, intervalExecsPerSecDouble, numValid, numTrials-numValid, nonZeroValidFraction); appendLineToFile(statsFile, plotData); } private int getTargetChildrenForParent(Input parentInput) { // Baseline is a constant int target = NUM_CHILDREN_BASELINE; // We like inputs that cover many things, so scale with fraction of max if (maxCoverage > 0) { target = (NUM_CHILDREN_BASELINE * parentInput.nonZeroCoverage) / maxCoverage; } // We absolutey love favored inputs, so fuzz them more if (parentInput.isFavored()) { target = target * NUM_CHILDREN_MULTIPLIER_FAVORED; } return target; } private void completeCycle() { // Increment cycle count cyclesCompleted++; infoLog("\n# Cycle " + cyclesCompleted + " completed."); // Go over all inputs and do a sanity check (plus log) infoLog("Here is a list of favored inputs:"); int sumResponsibilities = 0; numFavoredLastCycle = 0; for (Input input : savedInputs) { if (input.isFavored()) { int responsibleFor = input.responsibilities.size(); infoLog("Input %d is responsible for %d branches", input.id, responsibleFor); sumResponsibilities += responsibleFor; numFavoredLastCycle++; } } int totalCoverageCount = totalCoverage.getNonZeroCount(); infoLog("Total %d branches covered", totalCoverageCount); if (sumResponsibilities != totalCoverageCount) { throw new AssertionError("Responsibilty mistmatch"); } // Refresh ecToInputLoc so that subsequent splices are only from favored inputs ecToInputLoc.clear(); for (Input input : savedInputs) { if (input.isFavored()) { mapEcToInputLoc(input); } } // Break log after cycle infoLog("\n\n\n"); } @Override public InputStream getInput() throws GuidanceException { // Clear coverage stats for this run runCoverage.clear(); // Reset execution index state eiState = new ExecutionIndexingState(); // Choose an input to execute based on state of queues if (!seedInputs.isEmpty()) { // First, if we have some specific seeds, use those currentInput = seedInputs.removeFirst(); // Hopefully, the seeds will lead to new coverage and be added to saved inputs } else if (savedInputs.isEmpty()) { // If no seeds given try to start with something random if (!TOTALLY_RANDOM && numTrials > 100_000) { throw new GuidanceException("Too many trials without coverage; " + "likely all assumption violations"); } currentInput = new Input(); } else { // The number of children to produce is determined by how much of the coverage // pool this parent input hits Input currentParentInput = savedInputs.get(currentParentInputIdx); int targetNumChildren = getTargetChildrenForParent(currentParentInput); if (numChildrenGeneratedForCurrentParentInput >= targetNumChildren) { // Select the next saved input to fuzz currentParentInputIdx = (currentParentInputIdx + 1) % savedInputs.size(); // Count cycles if (currentParentInputIdx == 0) { completeCycle(); } numChildrenGeneratedForCurrentParentInput = 0; } Input parent = savedInputs.get(currentParentInputIdx); // Fuzz it to get a new input currentInput = parent.fuzz(random, ecToInputLoc); numChildrenGeneratedForCurrentParentInput++; // Write it to disk for debugging try { writeCurrentInputToFile(currentInputFile); } catch (IOException ignore) { } // Start time-counting for timeout handling this.runStart = new Date(); this.branchCount = 0; } // Return an input stream that uses the EI map return new InputStream() { int bytesRead = 0; @Override public int read() throws IOException { // lastEvent must not be null if (DISABLE_EXECUTION_INDEXING == false && lastEvent == null) { throw new IOException("Could not compute execution index; no instrumentation?"); } // Get the execution index of the last event ExecutionIndex executionIndex = DISABLE_EXECUTION_INDEXING ? new ExecutionIndex(new int[]{0, bytesRead}) : eiState.getExecutionIndex(lastEvent); // Attempt to get a value from the map, or else generate a random value int value = currentInput.getOrGenerateFresh(executionIndex, random); // Keep track of how many bytes were read in this input bytesRead++; return value; } }; } @Override public boolean hasInput() { Date now = new Date(); long elapsedMilliseconds = now.getTime() - startTime.getTime(); return elapsedMilliseconds < maxDurationMillis; } @Override public void handleResult(Result result, Throwable error) throws GuidanceException { // Stop timeout handling this.runStart = null; // Increment run count this.numTrials++; // Trim input (remove unused keys) currentInput.gc(); // It must still be non-empty assert(currentInput.valuesMap.size() > 0); boolean valid = result == Result.SUCCESS; if (valid) { // Increment valid counter numValid++; } if (result == Result.SUCCESS || result == Result.INVALID) { // Coverage before int nonZeroBefore = totalCoverage.getNonZeroCount(); int validNonZeroBefore = validCoverage.getNonZeroCount(); // Compute a list of keys for which this input can assume responsiblity. // Newly covered branches are always included. // Existing branches *may* be included, depending on the heuristics used. // A valid input will steal responsibility from invalid inputs Set responsibilities = computeResponsibilities(valid); // Update total coverage boolean coverageBitsUpdated = totalCoverage.updateBits(runCoverage); if (valid) { validCoverage.updateBits(runCoverage); } // Coverage after int nonZeroAfter = totalCoverage.getNonZeroCount(); if (nonZeroAfter > maxCoverage) { maxCoverage = nonZeroAfter; } int validNonZeroAfter = validCoverage.getNonZeroCount(); // Possibly save input boolean toSave = false; String why = ""; if (SAVE_NEW_COUNTS && coverageBitsUpdated) { toSave = true; why = why + "+count"; } // Save if new total coverage found if (nonZeroAfter > nonZeroBefore) { // Must be responsible for some branch assert(responsibilities.size() > 0); toSave = true; why = why + "+cov"; } if (validNonZeroAfter > validNonZeroBefore) { // Must be responsible for some branch assert(responsibilities.size() > 0); currentInput.valid = true; toSave = true; why = why + "+valid"; } if (toSave) { infoLog("Saving new input (at run %d): " + "input #%d " + "of size %d; " + "total coverage = %d", numTrials, savedInputs.size(), currentInput.valuesMap.size(), nonZeroAfter); // Save input to queue and to disk try { saveCurrentInput(responsibilities, why); } catch (IOException e) { throw new GuidanceException(e); } } } else if (result == Result.FAILURE || result == Result.TIMEOUT) { String msg = error.getMessage(); // Get the root cause of the failure Throwable rootCause = error; while (rootCause.getCause() != null) { rootCause = rootCause.getCause(); } // Attempt to add this to the set of unique failures if (uniqueFailures.add(Arrays.asList(rootCause.getStackTrace()))) { // Save crash to disk try { int crashIdx = uniqueFailures.size()-1; String saveFileName = String.format("id_%06d", crashIdx); File saveFile = new File(savedFailuresDirectory, saveFileName); writeCurrentInputToFile(saveFile); infoLog("%s","Found crash: " + error.getClass() + " - " + (msg != null ? msg : "")); String how = currentInput.desc; String why = result == Result.FAILURE ? "+crash" : "+hang"; infoLog("Saved - %s %s %s", saveFile.getPath(), how, why); } catch (IOException e) { throw new GuidanceException(e); } } } if (console != null) { displayStats(); } } // Compute a set of branches for which the current input may assume responsibility private Set computeResponsibilities(boolean valid) { Set result = new HashSet<>(); // This input is responsible for all new coverage Collection newCoverage = runCoverage.computeNewCoverage(totalCoverage); if (newCoverage.size() > 0) { result.addAll(newCoverage); } // If valid, this input is responsible for all new valid coverage if (valid) { Collection newValidCoverage = runCoverage.computeNewCoverage(validCoverage); if (newValidCoverage.size() > 0) { result.addAll(newValidCoverage); } } // Perhaps it can also steal responsibility from other inputs if (STEAL_RESPONSIBILITY) { int currentNonZeroCoverage = runCoverage.getNonZeroCount(); int currentInputSize = currentInput.size(); Set covered = new HashSet<>(runCoverage.getCovered()); // Search for a candidate to steal responsibility from candidate_search: for (Input candidate : savedInputs) { Set responsibilities = candidate.responsibilities; // Candidates with no responsibility are not interesting if (responsibilities.isEmpty()) { continue candidate_search; } // To avoid thrashing, only consider candidates with either // (1) strictly smaller total coverage or // (2) same total coverage but strictly larger size if (candidate.nonZeroCoverage < currentNonZeroCoverage || (candidate.nonZeroCoverage == currentNonZeroCoverage && currentInputSize < candidate.size())) { // Check if we can steal all responsibilities from candidate for (Object b : responsibilities) { if (covered.contains(b) == false) { // Cannot steal if this input does not cover something // that the candidate is responsible for continue candidate_search; } } // If all of candidate's responsibilities are covered by the // current input, then it can completely subsume the candidate result.addAll(responsibilities); } } } return result; } private void writeCurrentInputToFile(File saveFile) throws IOException { try (BufferedOutputStream out = new BufferedOutputStream(new FileOutputStream(saveFile))) { for (Object key : currentInput.orderedKeys) { int b = currentInput.valuesMap.get(key); assert (b >= 0 && b < 256); out.write(b); } } } private void saveCurrentInput(Set responsibilities, String why) throws IOException { // First, save to disk (note: we issue IDs to everyone, but only write to disk if valid) int newInputIdx = numSavedInputs++; String saveFileName = String.format("id_%06d", newInputIdx); String how = currentInput.desc; File saveFile = new File(savedInputsDirectory, saveFileName); if (SAVE_ONLY_VALID == false || currentInput.valid) { writeCurrentInputToFile(saveFile); infoLog("Saved - %s %s %s", saveFile.getPath(), how, why); } // If not using guidance, do nothing else if (TOTALLY_RANDOM) { return; } // Second, save to queue savedInputs.add(currentInput); // Third, store basic book-keeping data currentInput.id = newInputIdx; currentInput.saveFile = saveFile; currentInput.coverage = new Coverage(runCoverage); currentInput.nonZeroCoverage = runCoverage.getNonZeroCount(); currentInput.offspring = 0; savedInputs.get(currentParentInputIdx).offspring += 1; // Fourth, assume responsibility for branches currentInput.responsibilities = responsibilities; for (Object b : responsibilities) { // If there is an old input that is responsible, // subsume it Input oldResponsible = responsibleInputs.get(b); if (oldResponsible != null) { oldResponsible.responsibilities.remove(b); infoLog("-- Stealing responsibility for %s from input %d", b, oldResponsible.id); } else { infoLog("-- Assuming new responsibility for %s", b); } // We are now responsible responsibleInputs.put(b, currentInput); } // Fifth, map executions to input locations for splicing mapEcToInputLoc(currentInput); } private void mapEcToInputLoc(Input input) { for (int offset = 0; offset < input.size(); offset++) { ExecutionIndex ei = input.orderedKeys.get(offset); ExecutionContext ec = new ExecutionContext(ei); ecToInputLoc.get(ec).add(new InputLocation(input, offset)); } } @Override public Consumer generateCallBack(Thread thread) { if (appThread != null) { throw new IllegalStateException(ExecutionIndexingGuidance.class + " only supports single-threaded apps at the moment"); } appThread = thread; return this::handleEvent; } private void handleEvent(TraceEvent e) { // Set last event to this event lastEvent = e; // Update execution indexing logic if (!DISABLE_EXECUTION_INDEXING) { e.applyVisitor(this); } // Collect totalCoverage runCoverage.handleEvent(e); // Check for possible timeouts every so often if (this.singleRunTimeoutMillis > 0 && this.runStart != null && (++this.branchCount) % 10_000 == 0) { long elapsed = new Date().getTime() - runStart.getTime(); if (elapsed > this.singleRunTimeoutMillis) { throw new TimeoutException(elapsed, this.singleRunTimeoutMillis); } } } @Override public void visitCallEvent(CallEvent c) { eiState.pushCall(c); } @Override public void visitReturnEvent(ReturnEvent r) { eiState.popReturn(r); } /** * Returns a reference to the coverage statistics. * @return a reference to the coverage statistics */ public Coverage getTotalCoverage() { return totalCoverage; } /** * A candidate test input represented as a map from execution indices * to integer values. * *

When a quickcheck-like generator requests a new ``random'' byte, * the current execution index is used to retrieve the input from * this input map (a fresh value is generated and stored in the map * if the key is not mapped).

* *

Inputs should not be publicly mutable. The only way to mutate * an input is via the {@link #fuzz} method which produces a new input * object with some values mutated.

*/ public static class Input { /** A map from execution indexes to the byte (0-255) to be returned at that index. */ protected LinkedHashMap valuesMap; /** * A list of execution indexes that are actually requested by the test program when * executed with this input. * *

This list is initially empty, and is populated at the end of the run, after which * it is frozen. The list of keys are in order of their occurrence in the execution * trace and can therefore be used to serialize the map into a sequence of bytes.

* */ protected ArrayList orderedKeys = new ArrayList<>(); /** * Whether this input has been executed. * * When this field is {@code false}, the field {@link #orderedKeys} * is not yet populated and must not be used. When this field is {@code true}, * the input should be considered immutable and neither {@link #orderedKeys} nor * {@link #valuesMap} must be modified. */ protected boolean executed = false; /** * The file where this input is saved. * *

This field is null for inputs that are not saved.

*/ private File saveFile = null; /** * An ID for a saved input. * *

This field is -1 for inputs that are not saved.

*/ private int id; /** * The description for this input. * *

This field is modified by the construction and mutation * operations.

*/ protected String desc; /** * The run coverage for this input, if the input is saved. * *

This field is null for inputs that are not saved.

*/ private Coverage coverage = null; /** * The number of non-zero elements in `coverage`. * *

This field is -1 for inputs that are not saved.

* *

When this field is non-negative, the information is * redundant (can be computed using {@link Coverage#getNonZeroCount()}), * but we store it here for performance reasons.

*/ private int nonZeroCoverage = -1; /** * The number of mutant children spawned from this input that * were saved. * *

This field is -1 for inputs that are not saved.

*/ private int offspring = -1; /** * Whether this input resulted in a valid run. */ private boolean valid = false; /** * The set of coverage keys for which this input is * responsible. * *

This field is null for inputs that are not saved.

* *

Each coverage key appears in the responsibility set * of exactly one saved input, and all covered keys appear * in at least some responsibility set. Hence, this list * needs to be kept in-sync with {@link #responsibleInputs}.

*/ private Set responsibilities = null; private List demandDrivenSpliceMap = new ArrayList<>(); /** * Create an empty input map. */ public Input() { valuesMap = new LinkedHashMap<>(); desc = "random"; } /** * Create a copy of an existing input map. * * @param toClone the input map to clone */ public Input(Input toClone) { valuesMap = new LinkedHashMap<>(toClone.valuesMap); desc = String.format("src:%06d", toClone.id); } /** * Returns the size of this input, in terms of number of bytes * in its value map. * * @return the size of this input */ public final int size() { return valuesMap.size(); } /** * Returns the byte mapped by this input at a given offset. * * @param offset the byte offset in the input * @return the byte value at that offset * * @throws IndexOutOfBoundsException if the offset is negative or * larger than {@link #size}()-1 * @throws IllegalStateException if this method is called before the input * has been executed */ public final int getValueAtOffset(int offset) throws IndexOutOfBoundsException, IllegalStateException { if (!executed) { throw new IllegalStateException("Cannot get with offset before execution"); } // Return the mapping for the execution index queried at the offset ExecutionIndex ei = orderedKeys.get(offset); return valuesMap.get(ei); } /** * Returns the execution index mapped by this input at a given offset. * * @param offset the byte offset in the input * @return the execution index value at that offset * * @throws IndexOutOfBoundsException if the offset is negative or * larger than {@link #size}()-1 * @throws IllegalStateException if this method is called before the input * has been executed */ public final ExecutionIndex getKeyAtOffset(int offset) throws IndexOutOfBoundsException, IllegalStateException { if (!executed) { throw new IllegalStateException("Cannot get with offset before execution"); } // Return the execution index queried at the offset return orderedKeys.get(offset); } private InputPrefixMapping getInputPrefixMapping(ExecutionIndex ei) { for (InputPrefixMapping ipm : demandDrivenSpliceMap) { if (ei.hasPrefix(ipm.targetPrefix)) { return ipm; } } return null; } /** * Retrieve a value for an execution index if mapped, else generate * a fresh value. * * @param key the execution index of the trace event requesting a new byte * @param random the PRNG * @return the value to return to the quickcheck-like generator * @throws IllegalStateException if this method is called after the input * has been executed */ public int getOrGenerateFresh(ExecutionIndex key, Random random) throws IllegalStateException { if (executed) { throw new IllegalStateException("Cannot generate fresh values after execution"); } // If we reached a limit, then just return EOF if (orderedKeys.size() >= MAX_INPUT_SIZE) { return -1; } // Try to get existing values Integer val = valuesMap.get(key); // If not, generate a new value if (val == null) { InputPrefixMapping ipm; // If we have an input prefix mapping for this execution index, // then splice from the source input if ((ipm = getInputPrefixMapping(key)) != null) { Prefix sourcePrefix = ipm.sourcePrefix; Suffix sourceSuffix = ipm.sourcePrefix.getEi().getSuffixOfPrefix(sourcePrefix); ExecutionIndex sourceEi = new ExecutionIndex(sourcePrefix, sourceSuffix); // The value can be taken from the source val = ipm.sourceInput.getValueAtKey(sourceEi); } // If we could not splice or were unsuccessful, try to generate a new input if (val == null) { if (GENERATE_EOF_WHEN_OUT) { return -1; } if (random.nextDouble() < DEMAND_DRIVEN_SPLICING_PROBABILITY) { // TODO: Find a random inputLocation with same EC, // extract common suffix of sourceEi and targetEi, // and map targetPrefix to sourcePrefix in the IPM } else { // Just generate a random input val = random.nextInt(256); } } // Put the new value into the map assert (val != null); valuesMap.put(key, val); } // Mark this key as visited orderedKeys.add(key); return val; } /** * Gets the byte mapped by this input at a given execution index. * * @param ei the execution index * @return the value mapped for this index, or {@code null} if no such mapping exists * * @throws IndexOutOfBoundsException if the offset is negative or * larger than {@link #size}()-1 */ public final Integer getValueAtKey(ExecutionIndex ei) throws IndexOutOfBoundsException { return valuesMap.get(ei); } /** * Sets the byte mapped by this input at a given execution index. * * @param ei the execution index at which to insert * @param val the byte to insert * * @throws IndexOutOfBoundsException if the offset is negative or * larger than {@link #size}()-1 * @throws IllegalStateException if this method is called after the input * has been executed */ public final void setValueAtKey(ExecutionIndex ei, int val) throws IndexOutOfBoundsException, IllegalStateException { if (executed) { throw new IllegalStateException("Cannot set value before execution"); } valuesMap.put(ei, val); } /** * Trims the input map of all keys that were never actually requested since * its construction. * *

Although this operation mutates the underlying object, the effect should * not be externally visible (at least as long as the test executions are * deterministic).

*/ public void gc() { LinkedHashMap newMap = new LinkedHashMap<>(); for (ExecutionIndex key : orderedKeys) { newMap.put(key, valuesMap.get(key)); } valuesMap = newMap; // Set the `executed` flag executed = true; } /** * Return a new input derived from this one with some values * mutated. * *

This method performs one or both of random mutations * and splicing.

* *

Random mutations are done by performing M * mutation operations each on a random contiguous sequence of N bytes, * where M and N are sampled from a geometric distribution with mean * {@link #MEAN_MUTATION_COUNT} and {@link #MEAN_MUTATION_SIZE} * respectively.

* *

Splicing is performed by first randomly choosing a location and * its corresponding execution context in this input's value map, and then * copying a contiguous sequence of up to Z bytes from another input, * starting with a location that also maps the same execution context. * Here, Z is sampled from a uniform distribution from 0 to * {@link #MAX_SPLICE_SIZE}.

* * @param random the PRNG * @param ecToInputLoc a map of execution contexts to input locations * @return a newly fuzzed input */ public Input fuzz(Random random, Map> ecToInputLoc) { // Derive new input from this object as source Input newInput = new Input(this); // Maybe try splicing boolean splicingDone = false; // Only splice if we have been provided the ecToInputLoc if (ecToInputLoc != null) { // TODO: Do we really want splicing to be this frequent? if (random.nextBoolean()) { final int MIN_TARGET_ATTEMPTS = 3; final int MAX_TARGET_ATTEMPTS = 6; int targetAttempts = MIN_TARGET_ATTEMPTS; outer: for (int targetAttempt = 1; targetAttempt < targetAttempts; targetAttempt++) { // Choose an execution context at which to splice at // Note: We get EI and value from `this` rather than `newInput` // because `this` has already been executed int targetOffset = random.nextInt(newInput.valuesMap.size()); ExecutionIndex targetEi = this.getKeyAtOffset(targetOffset); ExecutionContext targetEc = new ExecutionContext(targetEi); int valueAtTarget = this.getValueAtOffset(targetOffset); // Find a suitable input location to splice from ArrayList inputLocations = ecToInputLoc.get(targetEc); // If this was a bad choice of target, try again without penalty if possible if (inputLocations.size() == 0) { // Try to increase the loop bound a little bit to get another chance targetAttempts = Math.min(targetAttempts+1, MAX_TARGET_ATTEMPTS); continue; } InputLocation inputLocation; // Try a bunch of times for (int attempt = 1; attempt <= 10; attempt++) { // Get a candidate source location with the same execution context inputLocation = inputLocations.get(random.nextInt(inputLocations.size())); Input sourceInput = inputLocation.input; int sourceOffset = inputLocation.offset; // Do not splice with ourselves if (sourceInput == this) { continue; } // Do not splice if the first value is the same in source and target if (sourceInput.getValueAtOffset(sourceOffset) == valueAtTarget) { continue; } int splicedBytes = 0; if (!DISABLE_EXECUTION_INDEXING && SPLICE_SUBTREE) { // Do not splice if there is no common suffix between EI of source and target ExecutionIndex sourceEi = sourceInput.getKeyAtOffset(sourceOffset); Suffix suffix = targetEi.getCommonSuffix(sourceEi); if (suffix.size() == 0) { continue; } // Extract the source and target prefixes Prefix sourcePrefix = sourceEi.getPrefixOfSuffix(suffix); Prefix targetPrefix = targetEi.getPrefixOfSuffix(suffix); assert (sourcePrefix.size() == targetPrefix.size()); // OK, this looks good. Let's splice! int srcIdx = sourceOffset; while (srcIdx < sourceInput.size()) { ExecutionIndex candidateEi = sourceInput.getKeyAtOffset(srcIdx); if (candidateEi.hasPrefix(sourcePrefix) == false) { // We are no more in the same sub-tree as sourceEi break; } Suffix spliceSuffix = candidateEi.getSuffixOfPrefix(sourcePrefix); ExecutionIndex spliceEi = new ExecutionIndex(targetPrefix, spliceSuffix); newInput.valuesMap.put(spliceEi, sourceInput.valuesMap.get(candidateEi)); srcIdx++; } splicedBytes = srcIdx - sourceOffset; } else { int spliceSize = 1 + random.nextInt(MAX_SPLICE_SIZE); int src = sourceOffset; int tgt = targetOffset; int srcSize = sourceInput.size(); int tgtSize = newInput.size(); while (splicedBytes < spliceSize && src < srcSize && tgt < tgtSize) { int val = sourceInput.getValueAtOffset(src); ExecutionIndex key = this.getKeyAtOffset(tgt); newInput.setValueAtKey(key, val); splicedBytes++; src++; tgt++; } } // Complete splicing splicingDone = true; newInput.desc += String.format(",splice:%06d:%d@%d->%d", sourceInput.id, splicedBytes, sourceOffset, targetOffset); break outer; // Stop more splicing attempts! } } } } // Maybe do random mutations if (splicingDone == false || random.nextBoolean()) { // Stack a bunch of mutations int numMutations = sampleGeometric(random, MEAN_MUTATION_COUNT); newInput.desc += ",havoc:"+numMutations; boolean setToZero = random.nextDouble() < 0.1; // one out of 10 times for (int mutation = 1; mutation <= numMutations; mutation++) { // Select a random offset and size int offset = random.nextInt(newInput.valuesMap.size()); int mutationSize = sampleGeometric(random, MEAN_MUTATION_SIZE); // desc += String.format(":%d@%d", mutationSize, idx); // Iterate over all entries in the value map Iterator> entryIterator = newInput.valuesMap.entrySet().iterator(); for (int i = 0; entryIterator.hasNext(); i++) { Map.Entry e = entryIterator.next(); // Only mutate `mutationSize` contiguous entries from // the randomly selected `idx`. if (i >= offset && i < (offset + mutationSize)) { // Apply a random mutation int mutatedValue = setToZero ? 0 : random.nextInt(256); e.setValue(mutatedValue); } } } } return newInput; } /** * Returns whether this input should be favored for fuzzing. * *

An input is favored if it is responsible for covering * at least one branch.

* * @return */ public boolean isFavored() { return responsibilities.size() > 0; } private int sampleGeometric(Random random, double mean) { double p = 1 / mean; double uniform = random.nextDouble(); return (int) ceil(log(1 - uniform) / log(1 - p)); } } public static class SeedInput extends Input { final File seedFile; final InputStream in; public SeedInput(File seedFile) throws IOException { this.seedFile = seedFile; this.in = new BufferedInputStream(new FileInputStream(seedFile)); this.desc = "seed"; } @Override public int getOrGenerateFresh(ExecutionIndex key, Random random) { int value; try { value = in.read(); } catch (IOException e) { throw new GuidanceException("Error reading from seed file: " + seedFile.getName(), e); } if (value >= 0) { valuesMap.put(key, value); orderedKeys.add(key); } return value; } @Override public void gc() { try { in.close(); } catch (IOException e) { throw new GuidanceException("Error closing seed file:" + seedFile.getName(), e); } } } static class InputLocation { private final Input input; private final int offset; InputLocation(Input input, int offset) { this.input = input; this.offset = offset; } } static class InputPrefixMapping { private final Input sourceInput; private final Prefix sourcePrefix; private final Prefix targetPrefix; InputPrefixMapping(Input sourceInput, Prefix sourcePrefix, Prefix targetPrefix) { this.sourceInput = sourceInput; this.sourcePrefix = sourcePrefix; this.targetPrefix = targetPrefix; } } }