Skip to content

Commit 34d7bd4

Browse files
committed
Implement Static Corpus Feature.
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.
1 parent f7dfd45 commit 34d7bd4

File tree

5 files changed

+79
-33
lines changed

5 files changed

+79
-33
lines changed

Sources/Fuzzilli/Configuration.swift

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,10 @@ public struct Configuration {
5353
/// as the reductions performed by the minimizer.
5454
public let enableInspection: Bool
5555

56+
/// Determines if we want to have a static corpus, i.e. we don't add any
57+
/// programs to the corpus even if they find new coverage.
58+
public let staticCorpus: Bool
59+
5660
public init(timeout: UInt32 = 250,
5761
skipStartupTests: Bool = false,
5862
logLevel: LogLevel = .info,
@@ -62,7 +66,8 @@ public struct Configuration {
6266
dropoutRate: Double = 0,
6367
collectRuntimeTypes: Bool = false,
6468
enableDiagnostics: Bool = false,
65-
enableInspection: Bool = false) {
69+
enableInspection: Bool = false,
70+
staticCorpus: Bool = false) {
6671
self.timeout = timeout
6772
self.logLevel = logLevel
6873
self.crashTests = crashTests
@@ -71,6 +76,7 @@ public struct Configuration {
7176
self.minimizationLimit = minimizationLimit
7277
self.enableDiagnostics = enableDiagnostics
7378
self.enableInspection = enableInspection
79+
self.staticCorpus = staticCorpus
7480
}
7581
}
7682

Sources/Fuzzilli/Corpus/BasicCorpus.swift

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,11 @@ public class BasicCorpus: ComponentBase, Collection, Corpus {
5757
}
5858

5959
override func initialize() {
60-
// Schedule a timer to perform cleanup regularly
61-
fuzzer.timers.scheduleTask(every: 30 * Minutes, cleanup)
60+
// Schedule a timer to perform cleanup regularly, only needed if we add
61+
// programs to our corpus.
62+
if !fuzzer.config.staticCorpus {
63+
fuzzer.timers.scheduleTask(every: 30 * Minutes, cleanup)
64+
}
6265
}
6366

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

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

8087
private func addInternal(_ program: Program) {

Sources/Fuzzilli/Fuzzer.swift

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,8 +74,10 @@ public class Fuzzer {
7474
case fuzzing
7575
}
7676

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

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

236+
// Set the fuzzer phase to .fuzzing.
237+
self.phase = .fuzzing
238+
234239
// When starting with an empty corpus, perform initial corpus generation using the GenerativeEngine.
235240
if corpus.isEmpty {
241+
if self.config.staticCorpus {
242+
logger.fatal("Corpus is empty after import in static corpus mode!")
243+
}
236244
logger.info("Empty corpus detected. Switching to the GenerativeEngine to perform initial corpus generation")
237245
startInitialCorpusGeneration()
238246
}
@@ -340,12 +348,18 @@ public class Fuzzer {
340348
/// some percentage of the programs if dropout is enabled.
341349
public func importCorpus(_ corpus: [Program], importMode: CorpusImportMode, enableDropout: Bool = false) {
342350
dispatchPrecondition(condition: .onQueue(queue))
351+
var timeOuts = 0
343352
for (count, program) in corpus.enumerated() {
344353
if count % 500 == 0 {
345354
logger.info("Imported \(count) of \(corpus.count)")
346355
}
347356
// Regardless of the import mode, we need to execute and evaluate the program first to update the evaluator state
348357
let execution = execute(program)
358+
359+
if execution.outcome == .timedOut {
360+
timeOuts += 1
361+
}
362+
349363
guard execution.outcome == .succeeded else { continue }
350364
let maybeAspects = evaluator.evaluate(execution)
351365

@@ -359,6 +373,10 @@ public class Fuzzer {
359373
}
360374
}
361375

376+
if timeOuts > 0 {
377+
logger.info("\(timeOuts)/\(corpus.count) samples timed out during import")
378+
}
379+
362380
if case .interestingOnly(let shouldMinimize) = importMode, shouldMinimize {
363381
// The corpus is being minimized now. Schedule a task to signal when the corpus import has really finished
364382
phase = .corpusImport

Sources/Fuzzilli/Modules/ThreadSync.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,13 +86,17 @@ public class ThreadWorker: Module {
8686

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

9091
// Register with the master.
91-
master.async {
92+
master.sync {
9293
guard let master = ThreadMaster.instance(for: master) else { fatalError("No active ThreadMaster module on master instance") }
9394
master.registerWorker(fuzzer)
95+
state = try! master.fuzzer.exportState()
9496
}
9597

98+
try! fuzzer.importState(from: state!)
99+
96100
fuzzer.registerEventListener(for: fuzzer.events.CrashFound) { ev in
97101
let program = ev.program.copy()
98102
master.async {

Sources/FuzzilliCli/main.swift

Lines changed: 37 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,10 @@ Options:
5050
--storagePath=path : Path at which to store output files (crashes, corpus, etc.) to.
5151
--resume : If storage path exists, import the programs from the corpus/ subdirectory
5252
--overwrite : If storage path exists, delete all data in it and start a fresh fuzzing session
53+
--staticCorpus : In this mode, we will just mutate the existing corpus and look for crashes.
54+
No new samples are added to the corpus, regardless of their coverage.
55+
This can be used to find different manifestations of bugs and
56+
also to try and reproduce a flaky crash or turn it into a deterministic one.
5357
--exportStatistics : If enabled, fuzzing statistics will be collected and saved to disk in regular intervals.
5458
Requires --storagePath.
5559
--statisticsExportInterval=n : Interval in minutes for saving fuzzing statistics to disk (default: 10).
@@ -135,6 +139,7 @@ let minimizationLimit = args.double(for: "--minimizationLimit") ?? 0.0
135139
let storagePath = args["--storagePath"]
136140
var resume = args.has("--resume")
137141
let overwrite = args.has("--overwrite")
142+
let staticCorpus = args.has("--staticCorpus")
138143
let exportStatistics = args.has("--exportStatistics")
139144
let statisticsExportInterval = args.uint(for: "--statisticsExportInterval") ?? 10
140145
let corpusImportAllPath = args["--importCorpusAll"]
@@ -185,10 +190,18 @@ if corpusImportAllPath != nil && corpusName == "markov" {
185190
configError("Markov corpus is not compatible with --importCorpusAll")
186191
}
187192

193+
if staticCorpus && !(resume || corpusImportAllPath != nil || corpusImportCovOnlyPath != nil || corpusImportMergePath != nil) {
194+
configError("Static corpus requires either --resume or one of the corpus import modes")
195+
}
196+
188197
if (resume || overwrite) && storagePath == nil {
189198
configError("--resume and --overwrite require --storagePath")
190199
}
191200

201+
if corpusName == "markov" && staticCorpus {
202+
configError("Markov corpus is not compatible with --staticCorpus")
203+
}
204+
192205
if let path = storagePath {
193206
let directory = (try? FileManager.default.contentsOfDirectory(atPath: path)) ?? []
194207
if !directory.isEmpty && !resume && !overwrite {
@@ -394,7 +407,8 @@ let config = Configuration(timeout: UInt32(timeout),
394407
isFuzzing: !dontFuzz,
395408
minimizationLimit: minimizationLimit,
396409
enableDiagnostics: diagnostics,
397-
enableInspection: inspect)
410+
enableInspection: inspect,
411+
staticCorpus: staticCorpus)
398412

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

@@ -464,31 +478,6 @@ fuzzer.sync {
464478
fuzzer.runStartupTests()
465479
}
466480

467-
// Add thread worker instances if requested
468-
//
469-
// This happens here, before any corpus is imported, so that any imported programs are
470-
// forwarded to the ThreadWorkers automatically when they are deemed interesting.
471-
//
472-
// This must *not* happen on the main fuzzer's queue since workers perform synchronous
473-
// operations on the master's dispatch queue.
474-
var instances = [fuzzer]
475-
for _ in 1..<numJobs {
476-
let worker = makeFuzzer(for: profile, with: config)
477-
instances.append(worker)
478-
let g = DispatchGroup()
479-
480-
g.enter()
481-
worker.sync {
482-
worker.addModule(Statistics())
483-
worker.addModule(ThreadWorker(forMaster: fuzzer))
484-
worker.registerEventListener(for: worker.events.Initialized) { g.leave() }
485-
worker.initialize()
486-
}
487-
488-
// Wait for the worker to be fully initialized
489-
g.wait()
490-
}
491-
492481
// Import a corpus if requested and start the main fuzzer instance.
493482
fuzzer.sync {
494483
func loadCorpus(from dirPath: String) -> [Program] {
@@ -554,6 +543,28 @@ fuzzer.sync {
554543
}
555544
}
556545

546+
// Add thread worker instances if requested
547+
//
548+
// This must *not* happen on the main fuzzer's queue since workers perform synchronous
549+
// operations on the master's dispatch queue.
550+
var instances = [fuzzer]
551+
for _ in 1..<numJobs {
552+
let worker = makeFuzzer(for: profile, with: config)
553+
instances.append(worker)
554+
let g = DispatchGroup()
555+
556+
g.enter()
557+
worker.sync {
558+
worker.addModule(Statistics())
559+
worker.addModule(ThreadWorker(forMaster: fuzzer))
560+
worker.registerEventListener(for: worker.events.Initialized) { g.leave() }
561+
worker.initialize()
562+
}
563+
564+
// Wait for the worker to be fully initialized
565+
g.wait()
566+
}
567+
557568
// Install signal handlers to terminate the fuzzer gracefully.
558569
var signalSources: [DispatchSourceSignal] = []
559570
for sig in [SIGINT, SIGTERM] {

0 commit comments

Comments
 (0)