pub struct GlobalCoverage {
    pub inner: Arc<RwLock<GlobalCoverageInner>>,
}
Expand description

Global coverage structure shared between threads and updated after each testcase run.

Role of Coverage in the Fuzzer

Coverage-guided fuzzing is used in a lot of modern fuzzers nowadays (e.g. LibFuzzer, HonggFuzz, etc.). The idea is to gather information during the execution of a program to identify which parts have actually been executed. The information gathered can be of different types (addresses, stack frames generated, etc.) and so do gathering methods (hardware mechanisms, hooks added at compile-time / runtime, etc.).

While using coverage is not necessary and can hinder performances, it’s a decent strategy for a generic fuzzer unaware of the input formats expected by the program, since the generated data can be used to identify interesting testcases (e.g. if they cover new paths).

Coverage Implementation

Coverage

Coverage is implemented in this fuzzer by hooking instructions that perform arbitrary or conditional branching, namely:

  • all the b.cond instructions;
  • cbz and cbnz;
  • tbz and tbnz;
  • blr and br.

The hooking function used for coverage is GlobalCoverage::hook_coverage and is placed while initializing the fuzzed program’s virtual space using GlobalCoverage::add_hooks.

Note: for more information on hooking, you can refer to Hooks.

Although, placing these hooks is a little bit complicated on ARM systems: it’s possible to have data sections, such as literal pools, wove into code sections. If we were to indiscriminately place hooks on any byte sequences that can be disassembled into the instructions listed above, we could corrupt one of these data sections, which might result in unwanted crashes or undefined behaviours. To prevent this from happening, the user is responsible for defining the ranges where coverage should be applied by implementing Loader::code_ranges from the Loader trait. This function will return a list of CoverageRanges defining the virtual address ranges that are safe to hook.

Once a program is running on a worker, coverage hooks that are hit take the current address and store it into a Coverage object. Each worker owns an instance of this structure. To make fuzzing more efficient, coverage information are shared between all workers using GlobalCoverage. At the end of each iteration, a worker take the coverage information that was generated from the current testcase and compare it to the global one. If new paths have been found, the testcase is added to the corpus so it can be reused and mutated to reach increasingly deeper execution paths.

Additionally, since the only information we’re interested in here is whether or not new paths have been hit, keeping hooks for paths that have already been covered is redundant. We can then get better performances by removing coverage hooks corresponding to new paths using Hooks::revert_coverage_hooks.

Comparison Unrolling

One of the main roadblocks to overcome while fuzzing is handling comparisons with constant magic values. For example, imagine that we have a program that checks that our input starts with 0xdeadbeef before processing it.

if u32::from_le_bytes(input[0..4]) == 0xdeadbeef {
    process();
} else {
    exit();
}

The fuzzer would have to guess 32 bits in a single iteration, which is very unlikely and would stall the fuzzer until it guesses it correctly.

There are different approaches possible to solve this problem, like hooking comparisons to always pass them or informing the mutator of the comparison value to generate a custom testcase with it. This fuzzer implements comparison unrolling. The idea is to take a comparison on a multi-byte value and split it into multiple single-byte comparisons.

if input[3] == 0xde {
    if input[2] == 0xad {
        if input[1] == 0xbe {
            // [...]
        }
    }
}

When the fuzzer is initialized, it looks for every comparison instructions it can find and then place a GlobalCoverage::hook_cmp hook on them. This function takes the hooked comparison instruction, disassemble it to retrieve the values being compared and adds a path for every byte it manages to guess correctly.

One of the issue with this implementation is that it adds new testcases for input values that partially match. Once we have a testcase that pass the comparison, the other intermediate testcases are less likely to produce interesting results and are mostly clogging up the input queue at this point. A possible way to improve the fuzzer in the future would be to add an additional queue for these intermediate testcases that would be flushed when the comparison is no longer an issue.

Fields

inner: Arc<RwLock<GlobalCoverageInner>>

Implementations

Instanciantes a new shared global coverage structure using user-provided virtual memory ranges where coverage should be applied.

Returns a copy of the inner coverage structure.

Returns the number of PCs covered.

Adds coverage hooks to the fuzzed program.

There are two hook types:

  • coverage hooks that updates the global coverage map;
  • comparison hooks that perform comparison unrolling and update the global coverage map when a new byte is matched.

Checks if the crash already exists in the list of known crashes and inserts it if it’s not the case. Returns true if the crash is new, false otherwise.

Checks if the coverage computed by the current worker adds new path to the global coverage. If it’s the case, the function adds the new paths covered to the global coverage map and returns true to signal that the current testcase yielded interesting results and should be added to the corpus.

Handles coverage hooks and adds the current instruction’s address to the local worker coverage.

Handles comparison hooks by disassembling the current instruction, retrieving the compared value and adding new paths based on how many bytes match.

Trait Implementations

Returns a copy of the value. Read more
Performs copy-assignment from source. Read more
Formats the value using the given formatter. Read more

Auto Trait Implementations

Blanket Implementations

Gets the TypeId of self. Read more
Immutably borrows from an owned value. Read more
Mutably borrows from an owned value. Read more

Returns the argument unchanged.

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

The resulting type after obtaining ownership.
Creates owned data from borrowed data, usually by cloning. Read more
Uses borrowed data to replace owned data, usually by cloning. Read more
The type returned in the event of a conversion error.
Performs the conversion.
The type returned in the event of a conversion error.
Performs the conversion.