Skip to content

Commit 91e3143

Browse files
committed
added resuming
1 parent a759317 commit 91e3143

4 files changed

Lines changed: 214 additions & 16 deletions

File tree

Sources/Fuzzilli/Corpus/PostgreSQLCorpus.swift

Lines changed: 60 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ public class PostgreSQLCorpus: ComponentBase, Corpus {
2525
private let databasePool: DatabasePool
2626
private let fuzzerInstanceId: String
2727
private let storage: PostgreSQLStorage
28+
private let resume: Bool
2829

2930
// MARK: - In-Memory Cache
3031

@@ -65,7 +66,8 @@ public class PostgreSQLCorpus: ComponentBase, Corpus {
6566
minMutationsPerSample: Int,
6667
databasePool: DatabasePool,
6768
fuzzerInstanceId: String,
68-
syncInterval: TimeInterval = 60.0 // Default 1 minute sync interval
69+
syncInterval: TimeInterval = 60.0, // Default 1 minute sync interval
70+
resume: Bool = true // Default to resume from previous state
6971
) {
7072
// The corpus must never be empty
7173
assert(minSize >= 1)
@@ -77,6 +79,7 @@ public class PostgreSQLCorpus: ComponentBase, Corpus {
7779
self.databasePool = databasePool
7880
self.fuzzerInstanceId = fuzzerInstanceId
7981
self.syncInterval = syncInterval
82+
self.resume = resume
8083
self.storage = PostgreSQLStorage(databasePool: databasePool)
8184

8285
self.programs = RingBuffer(maxSize: maxSize)
@@ -176,9 +179,11 @@ public class PostgreSQLCorpus: ComponentBase, Corpus {
176179
fuzzer.timers.scheduleTask(every: 30 * Minutes, cleanup)
177180
}
178181

179-
// Load initial corpus from database
180-
Task {
181-
await loadInitialCorpus()
182+
// Load initial corpus from database if resume is enabled
183+
if resume {
184+
Task {
185+
await loadInitialCorpus()
186+
}
182187
}
183188
}
184189

@@ -423,9 +428,55 @@ public class PostgreSQLCorpus: ComponentBase, Corpus {
423428
private func loadInitialCorpus() async {
424429
logger.info("Loading initial corpus from PostgreSQL...")
425430

426-
// This would be implemented when we have actual database operations
427-
// For now, just log that we would load from database
428-
logger.info("Initial corpus loading would be implemented here")
431+
guard let fuzzerId = fuzzerId else {
432+
logger.warning("Cannot load initial corpus: fuzzer not registered")
433+
return
434+
}
435+
436+
do {
437+
// Load programs from the last 24 hours to resume recent work
438+
let since = Date().addingTimeInterval(-24 * 60 * 60) // 24 hours ago
439+
let recentPrograms = try await storage.getRecentPrograms(
440+
fuzzerId: fuzzerId,
441+
since: since,
442+
limit: maxSize
443+
)
444+
445+
logger.info("Found \(recentPrograms.count) recent programs to resume")
446+
447+
// Add programs to the corpus
448+
cacheLock.lock()
449+
defer { cacheLock.unlock() }
450+
451+
for (program, metadata) in recentPrograms {
452+
let programHash = DatabaseUtils.calculateProgramHash(program: program)
453+
454+
// Skip if already in cache
455+
if programCache[programHash] != nil {
456+
continue
457+
}
458+
459+
// Add to in-memory structures
460+
prepareProgramForInclusion(program, index: totalEntryCounter)
461+
programs.append(program)
462+
ages.append(0)
463+
programHashes.append(programHash)
464+
programCache[programHash] = (program: program, metadata: metadata)
465+
466+
totalEntryCounter += 1
467+
}
468+
469+
logger.info("Resumed PostgreSQL corpus with \(programs.count) programs")
470+
471+
// If we have no programs, we need at least one to avoid empty corpus
472+
if programs.count == 0 {
473+
logger.info("No programs found to resume, corpus will start empty")
474+
}
475+
476+
} catch {
477+
logger.error("Failed to load initial corpus from PostgreSQL: \(error)")
478+
logger.info("Corpus will start empty and build up from scratch")
479+
}
429480
}
430481

431482
/// Synchronize with PostgreSQL database
@@ -489,7 +540,8 @@ public class PostgreSQLCorpus: ComponentBase, Corpus {
489540

490541
/// Register fuzzer with retry logic
491542
private func registerFuzzerWithRetry() async throws -> Int {
492-
let fuzzerName = "fuzzer-\(fuzzerInstanceId)"
543+
// Use the fuzzerInstanceId directly as the name to avoid double "fuzzer-" prefix
544+
let fuzzerName = fuzzerInstanceId
493545
let engineType = "v8" // This could be made configurable
494546

495547
let maxRetries = 3

Sources/Fuzzilli/Database/DatabaseUtils.swift

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -378,6 +378,24 @@ public enum DatabaseUtilsError: Error, LocalizedError {
378378
return "Invalid metadata format"
379379
}
380380
}
381+
382+
/// Map execution outcome string to database ID
383+
public static func mapExecutionOutcomeFromString(_ outcome: String) -> Int {
384+
switch outcome.lowercased() {
385+
case "crashed":
386+
return 1
387+
case "failed":
388+
return 2
389+
case "succeeded":
390+
return 3
391+
case "timedout":
392+
return 4
393+
case "sigcheck":
394+
return 34
395+
default:
396+
return 3 // Default to succeeded
397+
}
398+
}
381399
}
382400

383401
// MARK: - Extensions

Sources/Fuzzilli/Database/PostgreSQLStorage.swift

Lines changed: 134 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -51,20 +51,42 @@ public class PostgreSQLStorage {
5151
)
5252
defer { Task { _ = try? await connection.close() } }
5353

54-
let query: PostgresQuery = """
54+
// First, check if a fuzzer with this name already exists
55+
let checkQuery: PostgresQuery = "SELECT fuzzer_id, status FROM main WHERE fuzzer_name = \(name)"
56+
let checkResult = try await connection.query(checkQuery, logger: self.logger)
57+
let checkRows = try await checkResult.collect()
58+
59+
if let existingRow = checkRows.first {
60+
let existingFuzzerId = try existingRow.decode(Int.self, context: .default)
61+
let existingStatus = try existingRow.decode(String.self, context: .default)
62+
63+
// Update status to active if it was inactive
64+
if existingStatus != "active" {
65+
let updateQuery: PostgresQuery = "UPDATE main SET status = 'active' WHERE fuzzer_id = \(existingFuzzerId)"
66+
try await connection.query(updateQuery, logger: self.logger)
67+
logger.info("Reactivated existing fuzzer: fuzzerId=\(existingFuzzerId)")
68+
} else {
69+
logger.info("Reusing existing active fuzzer: fuzzerId=\(existingFuzzerId)")
70+
}
71+
72+
return existingFuzzerId
73+
}
74+
75+
// If no existing fuzzer found, create a new one
76+
let insertQuery: PostgresQuery = """
5577
INSERT INTO main (fuzzer_name, engine_type, status)
5678
VALUES (\(name), \(engineType), 'active')
5779
RETURNING fuzzer_id
5880
"""
5981

60-
let result = try await connection.query(query, logger: self.logger)
82+
let result = try await connection.query(insertQuery, logger: self.logger)
6183
let rows = try await result.collect()
6284
guard let row = rows.first else {
6385
throw PostgreSQLStorageError.noResult
6486
}
6587

6688
let fuzzerId = try row.decode(Int.self, context: .default)
67-
self.logger.info("Fuzzer registration successful: fuzzerId=\(fuzzerId)")
89+
self.logger.info("Created new fuzzer: fuzzerId=\(fuzzerId)")
6890
return fuzzerId
6991
}
7092

@@ -362,10 +384,115 @@ public class PostgreSQLStorage {
362384
public func getRecentPrograms(fuzzerId: Int, since: Date, limit: Int = 100) async throws -> [(Program, ExecutionMetadata)] {
363385
logger.info("Getting recent programs: fuzzerId=\(fuzzerId), since=\(since), limit=\(limit)")
364386

365-
// For now, return empty array
366-
// TODO: Implement actual database query when PostgreSQL is set up
367-
logger.info("Mock recent programs lookup: no programs found")
368-
return []
387+
guard let eventLoopGroup = databasePool.getEventLoopGroup() else {
388+
throw PostgreSQLStorageError.noResult
389+
}
390+
391+
let connection = try await PostgresConnection.connect(
392+
on: eventLoopGroup.next(),
393+
configuration: PostgresConnection.Configuration(
394+
host: "localhost",
395+
port: 5433,
396+
username: "fuzzilli",
397+
password: "fuzzilli123",
398+
database: "fuzzilli",
399+
tls: .disable
400+
),
401+
id: 0,
402+
logger: logger
403+
)
404+
defer { Task { _ = try? await connection.close() } }
405+
406+
// Query for recent programs with their latest execution metadata
407+
let queryString = """
408+
SELECT
409+
p.program_base64,
410+
p.program_size,
411+
p.program_hash,
412+
p.created_at,
413+
eo.outcome,
414+
eo.description,
415+
e.execution_time_ms,
416+
e.coverage_total,
417+
e.signal_code,
418+
e.exit_code
419+
FROM program p
420+
LEFT JOIN execution e ON p.program_base64 = e.program_base64
421+
LEFT JOIN execution_outcome eo ON e.execution_outcome_id = eo.id
422+
WHERE p.fuzzer_id = \(fuzzerId)
423+
AND p.created_at >= '\(since.ISO8601Format())'
424+
ORDER BY p.created_at DESC
425+
LIMIT \(limit)
426+
"""
427+
428+
let query = PostgresQuery(stringLiteral: queryString)
429+
let result = try await connection.query(query, logger: self.logger)
430+
let rows = try await result.collect()
431+
432+
var programs: [(Program, ExecutionMetadata)] = []
433+
434+
for row in rows {
435+
let programBase64 = try row.decode(String.self, context: .default)
436+
let programSize = try row.decode(Int.self, context: .default)
437+
let programHash = try row.decode(String.self, context: .default)
438+
let createdAt = try row.decode(Date.self, context: .default)
439+
let outcome = try row.decode(String?.self, context: .default)
440+
let description = try row.decode(String?.self, context: .default)
441+
let executionTimeMs = try row.decode(Int?.self, context: .default)
442+
let coverageTotal = try row.decode(Double?.self, context: .default)
443+
let signalCode = try row.decode(Int?.self, context: .default)
444+
let exitCode = try row.decode(Int?.self, context: .default)
445+
446+
// Decode the program from base64
447+
guard let programData = Data(base64Encoded: programBase64) else {
448+
logger.warning("Failed to decode base64 data for program: \(programHash)")
449+
continue
450+
}
451+
452+
let program: Program
453+
do {
454+
let protobuf = try Fuzzilli_Protobuf_Program(serializedBytes: programData)
455+
program = try Program(from: protobuf)
456+
} catch {
457+
logger.warning("Failed to decode program from protobuf: \(programHash), error: \(error)")
458+
continue
459+
}
460+
461+
// Create execution metadata
462+
463+
// Map outcome string to database ID
464+
let outcomeId: Int
465+
switch (outcome ?? "Succeeded").lowercased() {
466+
case "crashed":
467+
outcomeId = 1
468+
case "failed":
469+
outcomeId = 2
470+
case "succeeded":
471+
outcomeId = 3
472+
case "timedout":
473+
outcomeId = 4
474+
case "sigcheck":
475+
outcomeId = 34
476+
default:
477+
outcomeId = 3 // Default to succeeded
478+
}
479+
480+
let dbOutcome = DatabaseExecutionOutcome(
481+
id: outcomeId,
482+
outcome: outcome ?? "Succeeded",
483+
description: description ?? "Program executed successfully"
484+
)
485+
486+
var metadata = ExecutionMetadata(lastOutcome: dbOutcome)
487+
if let coverage = coverageTotal {
488+
metadata.lastCoverage = coverage
489+
}
490+
491+
programs.append((program, metadata))
492+
}
493+
494+
logger.info("Loaded \(programs.count) recent programs from database")
495+
return programs
369496
}
370497

371498
/// Update program metadata

Sources/FuzzilliCli/main.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -518,7 +518,8 @@ func makeFuzzer(with configuration: Configuration) -> Fuzzer {
518518
}
519519

520520
let databasePool = DatabasePool(connectionString: postgresUrl)
521-
let fuzzerInstanceId = "fuzzer-\(UUID().uuidString.prefix(8))"
521+
// Use a consistent fuzzer instance ID so we can resume with the same fuzzer
522+
let fuzzerInstanceId = "fuzzer-main"
522523

523524
corpus = PostgreSQLCorpus(
524525
minSize: minCorpusSize,

0 commit comments

Comments
 (0)