Skip to content

Commit

Permalink
Implement Static Corpus Feature.
Browse files Browse the repository at this point in the history
Implement a new mode for Fuzzilli called Static Corpus.
This allows Fuzzilli to Fuzz with a static corpus, i.e. Fuzzilli will
not add new programs to the corpus even if they create new coverage.
This can be useful to explore previously found programs that are
interesting, e.g. programs that exhibit flaky crashes.
  • Loading branch information
carl-smith committed Feb 3, 2023
1 parent f7dfd45 commit 34d7bd4
Show file tree
Hide file tree
Showing 5 changed files with 79 additions and 33 deletions.
8 changes: 7 additions & 1 deletion Sources/Fuzzilli/Configuration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ public struct Configuration {
/// as the reductions performed by the minimizer.
public let enableInspection: Bool

/// Determines if we want to have a static corpus, i.e. we don't add any
/// programs to the corpus even if they find new coverage.
public let staticCorpus: Bool

public init(timeout: UInt32 = 250,
skipStartupTests: Bool = false,
logLevel: LogLevel = .info,
Expand All @@ -62,7 +66,8 @@ public struct Configuration {
dropoutRate: Double = 0,
collectRuntimeTypes: Bool = false,
enableDiagnostics: Bool = false,
enableInspection: Bool = false) {
enableInspection: Bool = false,
staticCorpus: Bool = false) {
self.timeout = timeout
self.logLevel = logLevel
self.crashTests = crashTests
Expand All @@ -71,6 +76,7 @@ public struct Configuration {
self.minimizationLimit = minimizationLimit
self.enableDiagnostics = enableDiagnostics
self.enableInspection = enableInspection
self.staticCorpus = staticCorpus
}
}

Expand Down
13 changes: 10 additions & 3 deletions Sources/Fuzzilli/Corpus/BasicCorpus.swift
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,11 @@ public class BasicCorpus: ComponentBase, Collection, Corpus {
}

override func initialize() {
// Schedule a timer to perform cleanup regularly
fuzzer.timers.scheduleTask(every: 30 * Minutes, cleanup)
// Schedule a timer to perform cleanup regularly, only needed if we add
// programs to our corpus.
if !fuzzer.config.staticCorpus {
fuzzer.timers.scheduleTask(every: 30 * Minutes, cleanup)
}
}

public var size: Int {
Expand All @@ -74,7 +77,11 @@ public class BasicCorpus: ComponentBase, Collection, Corpus {
}

public func add(_ program: Program, _ : ProgramAspects) {
addInternal(program)
// We want to add programs either if we *don't* run with a static
// corpus at all or if we are importing a corpus.
if !fuzzer.config.staticCorpus || fuzzer.phase == .corpusImport {
addInternal(program)
}
}

private func addInternal(_ program: Program) {
Expand Down
22 changes: 20 additions & 2 deletions Sources/Fuzzilli/Fuzzer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,10 @@ public class Fuzzer {
case fuzzing
}

/// The current phase of the fuzzer
public private(set) var phase: Phase = .fuzzing
/// The current phase of the fuzzer, we set it to .corpusImport first,
/// which we may or may not do and then switch to .fuzzing once we actually
/// start fuzzing.
public private(set) var phase: Phase = .corpusImport

/// The modules active on this fuzzer.
var modules = [String: Module]()
Expand Down Expand Up @@ -231,8 +233,14 @@ public class Fuzzer {
private func startFuzzing() {
dispatchPrecondition(condition: .onQueue(queue))

// Set the fuzzer phase to .fuzzing.
self.phase = .fuzzing

// When starting with an empty corpus, perform initial corpus generation using the GenerativeEngine.
if corpus.isEmpty {
if self.config.staticCorpus {
logger.fatal("Corpus is empty after import in static corpus mode!")
}
logger.info("Empty corpus detected. Switching to the GenerativeEngine to perform initial corpus generation")
startInitialCorpusGeneration()
}
Expand Down Expand Up @@ -340,12 +348,18 @@ public class Fuzzer {
/// some percentage of the programs if dropout is enabled.
public func importCorpus(_ corpus: [Program], importMode: CorpusImportMode, enableDropout: Bool = false) {
dispatchPrecondition(condition: .onQueue(queue))
var timeOuts = 0
for (count, program) in corpus.enumerated() {
if count % 500 == 0 {
logger.info("Imported \(count) of \(corpus.count)")
}
// Regardless of the import mode, we need to execute and evaluate the program first to update the evaluator state
let execution = execute(program)

if execution.outcome == .timedOut {
timeOuts += 1
}

guard execution.outcome == .succeeded else { continue }
let maybeAspects = evaluator.evaluate(execution)

Expand All @@ -359,6 +373,10 @@ public class Fuzzer {
}
}

if timeOuts > 0 {
logger.info("\(timeOuts)/\(corpus.count) samples timed out during import")
}

if case .interestingOnly(let shouldMinimize) = importMode, shouldMinimize {
// The corpus is being minimized now. Schedule a task to signal when the corpus import has really finished
phase = .corpusImport
Expand Down
6 changes: 5 additions & 1 deletion Sources/Fuzzilli/Modules/ThreadSync.swift
Original file line number Diff line number Diff line change
Expand Up @@ -86,13 +86,17 @@ public class ThreadWorker: Module {

public func initialize(with fuzzer: Fuzzer) {
let master = self.master
var state: Data? = nil

// Register with the master.
master.async {
master.sync {
guard let master = ThreadMaster.instance(for: master) else { fatalError("No active ThreadMaster module on master instance") }
master.registerWorker(fuzzer)
state = try! master.fuzzer.exportState()
}

try! fuzzer.importState(from: state!)

fuzzer.registerEventListener(for: fuzzer.events.CrashFound) { ev in
let program = ev.program.copy()
master.async {
Expand Down
63 changes: 37 additions & 26 deletions Sources/FuzzilliCli/main.swift
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ Options:
--storagePath=path : Path at which to store output files (crashes, corpus, etc.) to.
--resume : If storage path exists, import the programs from the corpus/ subdirectory
--overwrite : If storage path exists, delete all data in it and start a fresh fuzzing session
--staticCorpus : In this mode, we will just mutate the existing corpus and look for crashes.
No new samples are added to the corpus, regardless of their coverage.
This can be used to find different manifestations of bugs and
also to try and reproduce a flaky crash or turn it into a deterministic one.
--exportStatistics : If enabled, fuzzing statistics will be collected and saved to disk in regular intervals.
Requires --storagePath.
--statisticsExportInterval=n : Interval in minutes for saving fuzzing statistics to disk (default: 10).
Expand Down Expand Up @@ -135,6 +139,7 @@ let minimizationLimit = args.double(for: "--minimizationLimit") ?? 0.0
let storagePath = args["--storagePath"]
var resume = args.has("--resume")
let overwrite = args.has("--overwrite")
let staticCorpus = args.has("--staticCorpus")
let exportStatistics = args.has("--exportStatistics")
let statisticsExportInterval = args.uint(for: "--statisticsExportInterval") ?? 10
let corpusImportAllPath = args["--importCorpusAll"]
Expand Down Expand Up @@ -185,10 +190,18 @@ if corpusImportAllPath != nil && corpusName == "markov" {
configError("Markov corpus is not compatible with --importCorpusAll")
}

if staticCorpus && !(resume || corpusImportAllPath != nil || corpusImportCovOnlyPath != nil || corpusImportMergePath != nil) {
configError("Static corpus requires either --resume or one of the corpus import modes")
}

if (resume || overwrite) && storagePath == nil {
configError("--resume and --overwrite require --storagePath")
}

if corpusName == "markov" && staticCorpus {
configError("Markov corpus is not compatible with --staticCorpus")
}

if let path = storagePath {
let directory = (try? FileManager.default.contentsOfDirectory(atPath: path)) ?? []
if !directory.isEmpty && !resume && !overwrite {
Expand Down Expand Up @@ -394,7 +407,8 @@ let config = Configuration(timeout: UInt32(timeout),
isFuzzing: !dontFuzz,
minimizationLimit: minimizationLimit,
enableDiagnostics: diagnostics,
enableInspection: inspect)
enableInspection: inspect,
staticCorpus: staticCorpus)

let fuzzer = makeFuzzer(for: profile, with: config)

Expand Down Expand Up @@ -464,31 +478,6 @@ fuzzer.sync {
fuzzer.runStartupTests()
}

// Add thread worker instances if requested
//
// This happens here, before any corpus is imported, so that any imported programs are
// forwarded to the ThreadWorkers automatically when they are deemed interesting.
//
// This must *not* happen on the main fuzzer's queue since workers perform synchronous
// operations on the master's dispatch queue.
var instances = [fuzzer]
for _ in 1..<numJobs {
let worker = makeFuzzer(for: profile, with: config)
instances.append(worker)
let g = DispatchGroup()

g.enter()
worker.sync {
worker.addModule(Statistics())
worker.addModule(ThreadWorker(forMaster: fuzzer))
worker.registerEventListener(for: worker.events.Initialized) { g.leave() }
worker.initialize()
}

// Wait for the worker to be fully initialized
g.wait()
}

// Import a corpus if requested and start the main fuzzer instance.
fuzzer.sync {
func loadCorpus(from dirPath: String) -> [Program] {
Expand Down Expand Up @@ -554,6 +543,28 @@ fuzzer.sync {
}
}

// Add thread worker instances if requested
//
// This must *not* happen on the main fuzzer's queue since workers perform synchronous
// operations on the master's dispatch queue.
var instances = [fuzzer]
for _ in 1..<numJobs {
let worker = makeFuzzer(for: profile, with: config)
instances.append(worker)
let g = DispatchGroup()

g.enter()
worker.sync {
worker.addModule(Statistics())
worker.addModule(ThreadWorker(forMaster: fuzzer))
worker.registerEventListener(for: worker.events.Initialized) { g.leave() }
worker.initialize()
}

// Wait for the worker to be fully initialized
g.wait()
}

// Install signal handlers to terminate the fuzzer gracefully.
var signalSources: [DispatchSourceSignal] = []
for sig in [SIGINT, SIGTERM] {
Expand Down

0 comments on commit 34d7bd4

Please sign in to comment.