Skip to content

Commit

Permalink
Implemented new approach for fuzzer synchronization
Browse files Browse the repository at this point in the history
This commit changes the way fuzzer workers synchronize. Previously, a new
worker would be sent the entire set of interesting samples every discovered by
the responsible master. It would then execute all of them and thus be
synchronized. This had the advantage of filtering out non-deterministic samples
but obviously was very slow for large corpuses.
With this commit, the fuzzer master now exports its internal state consisting of
the state of its evaluator (e.g. the coverage bitmap for edge coverage) and the
current (active) corpus. It then sends both to the worker which simply imports
them without having to execute any programs. The worker is then basically a
clone of the master. Afterwards, workers will (hopefully) diverge from the
master due to dropout and non-deterministic samples.
This now also makes it possible to limit the size of the corpus, e.g. to avoid
OOMing if too many intersting samples are discovered.
  • Loading branch information
Samuel Groß committed Jun 17, 2019
1 parent 498ccbd commit 5aeae9d
Show file tree
Hide file tree
Showing 26 changed files with 633 additions and 372 deletions.
12 changes: 0 additions & 12 deletions Sources/Fuzzilli/Configuration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,16 +36,6 @@ public struct Configuration {
/// Is this instance configured to run as a worker?
public let isWorker: Bool

/// Whether to use fast worker synchronization.
/// If enabled, master instances will, alongside the corpus, also send the internal state
/// of their evaluator to workers. The workers can then directly import that state without
/// having to execute all samples of the corpus. This does require masters and workers
/// to be basically identical (e.g. same build of the target etc.) and might result in more
/// non-deterministic samples in the corpus (which would otherwise be filtered out during
/// re-execution of the samples).
/// This option is recommended when dealing with large (maybe >=25k samples) corpuses.
public let fastWorkerSync: Bool

/// The minimum number of instructions that programs which are put into the corpus should have.
/// This setting is useful to avoid "over-minimization", which can negatively impact the fuzzer's
/// performance if program features are removed that could later be mutated to trigger new
Expand All @@ -66,7 +56,6 @@ public struct Configuration {
crashTests: [String] = [],
isMaster: Bool = false,
isWorker: Bool = false,
fastWorkerSync: Bool = false,
minimizationLimit: UInt = 0,
dropoutRate: Double = 0.01) {
self.timeout = timeout
Expand All @@ -75,7 +64,6 @@ public struct Configuration {
self.crashTests = crashTests
self.isMaster = isMaster
self.isWorker = isWorker
self.fastWorkerSync = fastWorkerSync
self.dropoutRate = dropoutRate
self.minimizationLimit = minimizationLimit
}
Expand Down
85 changes: 56 additions & 29 deletions Sources/Fuzzilli/Core/Corpus.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,18 @@
// See the License for the specific language governing permissions and
// limitations under the License.

/// Corpus for mutation-based fuzzing.
///
/// The corpus contains FuzzIL programs that can be used as input for mutations.
/// Any newly found interesting program is added to the corpus.
/// Programs are evicted from the copus for two reasons:
///
/// - if the corpus grows too large (larger than maxCorpusSize), in which
/// case the oldest programs are removed.
/// - if a program has been mutated often enough (at least
/// minMutationsPerSample times).
///
/// However, once reached, the corpus will never shrink below minCorpusSize again.
public class Corpus: ComponentBase {
/// The minimum number of samples that should be kept in the corpus.
private let minSize: Int
Expand All @@ -20,17 +32,18 @@ public class Corpus: ComponentBase {
/// for mutation before it can be discarded from the active set.
private let minMutationsPerSample: Int

/// All interesting programs ever found.
private var all: [Program]

/// The current set of interesting programs used for mutations.
private var active: [(program: Program, age: Int)]
private var programs: RingBuffer<Program>
private var ages: RingBuffer<Int>

public init(minSize: Int, minMutationsPerSample: Int) {
public init(minSize: Int, maxSize: Int, minMutationsPerSample: Int) {
assert(maxSize >= minSize)

self.minSize = minSize
self.minMutationsPerSample = minMutationsPerSample
self.all = []
self.active = []

self.programs = RingBuffer(maxSize: maxSize)
self.ages = RingBuffer(maxSize: maxSize)

super.init(name: "Corpus")
}
Expand All @@ -46,52 +59,66 @@ public class Corpus: ComponentBase {
}

public var size: Int {
return active.count
return programs.count
}

public var isEmpty: Bool {
return size == 0
}

/// Exports the entire corpus as a list of programs.
public func export() -> [Program] {
return all
}

/// Adds a program to the corpus.
func add(_ program: Program) {
public func add(_ program: Program) {
if program.size > 0 {
active.append((program, 0))
all.append(program)
programs.append(program)
ages.append(0)
}
}

/// Adds multiple programs to the corpus.
func add(_ programs: [Program]) {
public func add(_ programs: [Program]) {
programs.forEach(add)
}

func takeSample(count: Bool = true) -> Program {
let idx = Int.random(in: 0..<active.count)
if count {
active[idx].age += 1
/// Returns a random program from this corpus and potentially increases its age by one.
public func randomElement(increaseAge: Bool = true) -> Program {
let idx = Int.random(in: 0..<programs.count)
if increaseAge {
ages[idx] += 1
}
let program = active[idx].program
let program = programs[idx]
assert(!program.isEmpty)
return program
}

public func exportState() -> [Program] {
return [Program](programs)
}

public func importState(_ state: [Program]) throws {
guard state.count > 0 else {
throw RuntimeError("Cannot import an empty corpus.")
}

self.programs.removeAll()
self.ages.removeAll()

state.forEach(add)
}

private func cleanup() {
var newSamples = [(Program, Int)]()
var newPrograms = RingBuffer<Program>(maxSize: programs.maxSize)
var newAges = RingBuffer<Int>(maxSize: ages.maxSize)

for i in 0..<active.count {
let remaining = active.count - i
if active[i].age < minMutationsPerSample || remaining <= (minSize - newSamples.count) {
newSamples.append(active[i])
for i in 0..<programs.count {
let remaining = programs.count - i
if ages[i] < minMutationsPerSample || remaining <= (minSize - newPrograms.count) {
newPrograms.append(programs[i])
newAges.append(ages[i])
}
}

logger.info("Corpus cleanup finished: \(self.active.count) -> \(newSamples.count)")
active = newSamples
logger.info("Corpus cleanup finished: \(self.programs.count) -> \(newPrograms.count)")
programs = newPrograms
ages = newAges
}
}
7 changes: 6 additions & 1 deletion Sources/Fuzzilli/Core/Events.swift
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,14 @@ public class Events {

/// Signals that a this instance is shutting down.
public let Shutdown = Event<Void>()

/// Signals that this instance has successfully shut down.
/// This is useful for embedders to e.g. terminate the fuzzer process on completion.
public let ShutdownComplete = Event<Void>()

/// Signals that a log message was dispatched.
public let Log = Event<(level: LogLevel, instance: Int, label: String, message: String)>()
/// The creator field is the UUID of the fuzzer instance that originally created the message.
public let Log = Event<(creator: UUID, level: LogLevel, label: String, message: String)>()

/// Signals that a new (mutated) program has been generated.
public let ProgramGenerated = Event<Program>()
Expand Down
4 changes: 2 additions & 2 deletions Sources/Fuzzilli/Core/FuzzerCore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ public class FuzzerCore: ComponentBase {
/// This ensures that samples will be mutated multiple times as long
/// as the intermediate results do not cause a runtime exception.
func fuzzOne() {
var parent = prepareForMutation(fuzzer.corpus.takeSample())
var parent = prepareForMutation(fuzzer.corpus.randomElement())
var program = Program()

for _ in 0..<numConsecutiveMutations {
Expand All @@ -142,7 +142,7 @@ public class FuzzerCore: ComponentBase {
mutated = true
break
}
logger.verbose("\(mutator.name) failed")
logger.verbose("\(mutator.name) failed, trying different mutator")
mutator = chooseUniform(from: mutators)

}
Expand Down
15 changes: 8 additions & 7 deletions Sources/Fuzzilli/Core/Logging.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,23 +23,23 @@ public enum LogLevel: Int {
}

public class Logger {
typealias LogEvent = Event<(level: LogLevel, instance: Int, label: String, message: String)>
typealias LogEvent = Event<(creator: UUID, level: LogLevel, label: String, message: String)>

private let creator: UUID
private let event: LogEvent
private let instance: Int
private let label: String
private let minLevel: LogLevel

init(logEvent: LogEvent, instance: Int, label: String, minLevel: LogLevel) {
init(creator: UUID, logEvent: LogEvent, label: String, minLevel: LogLevel) {
self.creator = creator
self.event = logEvent
self.instance = instance
self.label = label
self.minLevel = minLevel
}

private func log(level: LogLevel, msg: String) {
if minLevel.rawValue <= level.rawValue {
dispatchEvent(event, data: (level: level, instance: instance, label: label, message: msg))
dispatchEvent(event, data: (creator: creator, level: level, label: label, message: msg))
}
}

Expand All @@ -66,7 +66,8 @@ public class Logger {
/// Log a message with log level fatal. This will afterwards terminate the application.
public func fatal(_ msg: String) -> Never {
log(level: .fatal, msg: msg)
/// TODO shut down the fuzzer first?
exit(-1)
// We don't really want to do proper cleanup here as the fuzzer's internal state could be corupted.
// As such, just kill the entire process here...
abort()
}
}
13 changes: 4 additions & 9 deletions Sources/Fuzzilli/Evaluation/ProgramCoverageEvaluator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,6 @@ public class ProgramCoverageEvaluator: ComponentBase, ProgramEvaluator {
return Double(context.found_edges) / Double(context.num_edges)
}

/// Whether an existing state has been imported.
public private(set) var hasImportedState = false

/// Context for the C library.
private var context = libcoverage.cov_context()

Expand Down Expand Up @@ -133,24 +130,23 @@ public class ProgramCoverageEvaluator: ComponentBase, ProgramEvaluator {
return state
}

public func importState(_ state: Data) {
public func importState(_ state: Data) throws {
assert(isInitialized)

guard state.count == 24 + context.bitmap_size * 2 else {
return logger.error("Cannot import coverage state. Ensure all instances use the same build of the target")
throw RuntimeError("Cannot import coverage state as it has an unexpected size. Ensure all instances use the same build of the target")
}

let numEdges = state.withUnsafeBytes { $0.load(fromByteOffset: 0, as: UInt64.self) }
let bitmapSize = state.withUnsafeBytes { $0.load(fromByteOffset: 8, as: UInt64.self) }
let foundEdges = state.withUnsafeBytes { $0.load(fromByteOffset: 16, as: UInt64.self) }

guard bitmapSize == context.bitmap_size && numEdges == context.num_edges else {
return logger.error("Cannot import coverage state. Ensure all instances use the same build of the target")
throw RuntimeError("Cannot import coverage state due to different bitmap sizes. Ensure all instances use the same build of the target")
}

if foundEdges < context.found_edges {
// This might happen if a master instance crashes and restarts and workers reconnect to it.
return logger.warning("Not importing coverage state as it has less found edges than ours")
return logger.info("Not importing coverage state as it has less found edges than ours")
}

context.found_edges = foundEdges
Expand All @@ -161,6 +157,5 @@ public class ProgramCoverageEvaluator: ComponentBase, ProgramEvaluator {
state.copyBytes(to: context.crash_bits, from: start..<start + Int(bitmapSize))

logger.info("Imported existing coverage state with \(foundEdges) edges already discovered")
hasImportedState = true
}
}
5 changes: 1 addition & 4 deletions Sources/Fuzzilli/Evaluation/ProgramEvaluator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,5 @@ public protocol ProgramEvaluator: Component {
func exportState() -> Data

/// Import a previously exported state.
func importState(_ state: Data)

/// Whether an existing state has successfully been imported previously.
var hasImportedState: Bool { get }
func importState(_ state: Data) throws
}
4 changes: 1 addition & 3 deletions Sources/Fuzzilli/FuzzIL/Program.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,7 @@ public class Program: Collection, Codable {
}

/// The index of the first instruction, always 0.
public var startIndex: Int {
return 0
}
public let startIndex = 0

/// The index of the last instruction plus one, always equal to the size of the program.
public var endIndex: Int {
Expand Down
Loading

0 comments on commit 5aeae9d

Please sign in to comment.