From bf0ac484a1277108625db22caf07e15ad018387a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20Gro=C3=9F?= Date: Tue, 10 Dec 2024 18:22:40 +0100 Subject: [PATCH] Improve corpus import fixup further With the preceeding change, we can now import failing programs by wrapping them in a big try-catch statement. This is somewhat inefficient, however, as we may end up with samples where a lot of the code is never executed (because an exception is thrown early on). This change now adds additional logic to improve that and ensure that as much code as possible will also be executed at runtime. Specifically, the fixup algorithm when importing programs now works like this: 1. We first try to replace known test functions (such as `assertEquals`) with a dummy function as these test functions aren't available outside the testing environment. This now replaces a mechanism in the compiler that would also try to remove calls to these functions. 2. If the first attempt fails, we'll then attempt to insert try-catch blocks around individual instructions. For that, we first enable all guardable operations (which will cause them to be wrapped in try-catch), then perform one round of fixup during which all unecessary guards will be disabled again. As a result, only those try-catch block that are necessary will stay in the imported program. 3. Only if the previous attempts fail do we now insert one big try-catch statement around the entire program. --- Sources/FuzzILTool/main.swift | 16 +- Sources/Fuzzilli/Compiler/Compiler.swift | 52 +--- Sources/Fuzzilli/Execution/Execution.swift | 8 + Sources/Fuzzilli/FuzzIL/JsOperations.swift | 84 ++++++ Sources/Fuzzilli/Fuzzer.swift | 247 ++++++++++++++---- .../Minimization/SimplifyingReducer.swift | 70 +---- Sources/Fuzzilli/Modules/Sync.swift | 6 +- Sources/Fuzzilli/Mutators/FixupMutator.swift | 6 +- 8 files changed, 306 insertions(+), 183 deletions(-) diff --git a/Sources/FuzzILTool/main.swift b/Sources/FuzzILTool/main.swift index 30ab147fc..21bac202e 100644 --- a/Sources/FuzzILTool/main.swift +++ b/Sources/FuzzILTool/main.swift @@ -24,20 +24,6 @@ let jsSuffix = "" let jsLifter = JavaScriptLifter(prefix: jsPrefix, suffix: jsSuffix, ecmaVersion: ECMAScriptVersion.es6) let fuzzILLifter = FuzzILLifter() -// Default list of functions that are filtered out during compilation. These are functions that may be used in testcases but which do not influence the test's behaviour and so should be omitted for fuzzing. -// The functions can use the wildcard '*' character as _last_ character, in which case a prefix match will be performed. -let filteredFunctionsForCompiler = [ - // Functions used in V8's test suite - "assert*", - "print*", - // Functions used in Mozilla's test suite - "startTest", - "enterFunc", - "exitFunc", - "report*", - "options*", -] - // Loads a serialized FuzzIL program from the given file func loadProgram(from path: String) throws -> Program { let data = try Data(contentsOf: URL(fileURLWithPath: path)) @@ -188,7 +174,7 @@ else if args.has("--compile") { exit(-1) } - let compiler = JavaScriptCompiler(deletingCallTo: filteredFunctionsForCompiler) + let compiler = JavaScriptCompiler() let program: Program do { program = try compiler.compile(ast) diff --git a/Sources/Fuzzilli/Compiler/Compiler.swift b/Sources/Fuzzilli/Compiler/Compiler.swift index b67142a17..8c476c6e1 100644 --- a/Sources/Fuzzilli/Compiler/Compiler.swift +++ b/Sources/Fuzzilli/Compiler/Compiler.swift @@ -27,22 +27,11 @@ public class JavaScriptCompiler { case unsupportedFeatureError(String) } - public init(deletingCallTo filteredFunctions: [String] = []) { - self.filteredFunctions = filteredFunctions - } + public init() {} /// The compiled code. private var code = Code() - /// A list of function names or prefixes (e.g. `assert*`) which should be deleted from the output program. - /// The function calls can in general only be removed if their return value isn't used, and so currently they are only - /// removed if they make up a full ExpressionStatement, in which case the entire statement is ignored. - /// This functionality is useful to remove calls to functions such as `assert*` or `print*` from tests - /// as those are not useful for fuzzing. - /// The function names may contain the wildcard character `*`, but _only_ as last character, in which case - /// a prefix match will be performed instead of a string comparison. - private let filteredFunctions: [String] - /// The environment is used to determine if an identifier identifies a builtin object. /// TODO we should probably use the correct target environment, with any additional builtins etc. here. But for now, we just manually add `gc` since that's relatively common. private var environment = JavaScriptEnvironment(additionalBuiltins: ["gc": .function()]) @@ -82,11 +71,6 @@ public class JavaScriptCompiler { } private func compileStatement(_ node: StatementNode) throws { - let shouldIgnoreStatement = try performStatementFiltering(node) - guard !shouldIgnoreStatement else { - return - } - guard let stmt = node.statement else { throw CompilerError.invalidASTError("missing concrete statement in statement node") } @@ -1239,40 +1223,6 @@ public class JavaScriptCompiler { return (variables, spreads) } - /// Determine whether the given statement should be filtered out. - /// - /// Currently this function only performs function call filtering based on the `filteredFunctions` array. - private func performStatementFiltering(_ statement: StatementNode) throws -> Bool { - guard case .expressionStatement(let expressionStatement) = statement.statement else { return false } - guard case .callExpression(let callExpression) = expressionStatement.expression.expression else { return false } - guard case .identifier(let identifier) = callExpression.callee.expression else { return false } - - let functionName = identifier.name - var shouldIgnore = false - for filteredFunction in filteredFunctions { - if filteredFunction.last == "*" { - if functionName.starts(with: filteredFunction.dropLast()) { - shouldIgnore = true - } - } else { - assert(!filteredFunction.contains("*")) - if functionName == filteredFunction { - shouldIgnore = true - } - } - } - - if shouldIgnore { - // Still generate code for the arguments. - // For example, we may still want to emit the function call for something like `assertEq(f(), 42);` - for arg in callExpression.arguments { - try compileExpression(arg) - } - } - - return shouldIgnore - } - private func reset() { code = Code() scopes.removeAll() diff --git a/Sources/Fuzzilli/Execution/Execution.swift b/Sources/Fuzzilli/Execution/Execution.swift index 746b446fc..5d23c70a2 100644 --- a/Sources/Fuzzilli/Execution/Execution.swift +++ b/Sources/Fuzzilli/Execution/Execution.swift @@ -41,6 +41,14 @@ public enum ExecutionOutcome: CustomStringConvertible, Equatable, Hashable { return false } } + + public func isFailure() -> Bool { + if case .failed = self { + return true + } else { + return false + } + } } /// The result of executing a program. diff --git a/Sources/Fuzzilli/FuzzIL/JsOperations.swift b/Sources/Fuzzilli/FuzzIL/JsOperations.swift index 6bf9dbcf0..62b7a4497 100644 --- a/Sources/Fuzzilli/FuzzIL/JsOperations.swift +++ b/Sources/Fuzzilli/FuzzIL/JsOperations.swift @@ -55,6 +55,90 @@ class GuardableOperation: JsOperation { self.isGuarded = isGuarded super.init(numInputs: numInputs, numOutputs: numOutputs, numInnerOutputs: numInnerOutputs, firstVariadicInput: firstVariadicInput, attributes: attributes, requiredContext: requiredContext) } + + // Helper functions to enable guards. + // If the given operation already has guarding enabled, then this function does + // nothing and simply returns the input. Otherwise it creates a copy of the + // operations which has guarding enabled. + static func enableGuard(of operation: GuardableOperation) -> GuardableOperation { + if operation.isGuarded { + return operation + } + switch operation.opcode { + case .getProperty(let op): + return GetProperty(propertyName: op.propertyName, isGuarded: true) + case .deleteProperty(let op): + return DeleteProperty(propertyName: op.propertyName, isGuarded: true) + case .getElement(let op): + return GetElement(index: op.index, isGuarded: true) + case .deleteElement(let op): + return DeleteElement(index: op.index, isGuarded: true) + case .getComputedProperty: + return GetComputedProperty(isGuarded: true) + case .deleteComputedProperty: + return DeleteComputedProperty(isGuarded: true) + case .callFunction(let op): + return CallFunction(numArguments: op.numArguments, isGuarded: true) + case .callFunctionWithSpread(let op): + return CallFunctionWithSpread(numArguments: op.numArguments, spreads: op.spreads, isGuarded: true) + case .construct(let op): + return Construct(numArguments: op.numArguments, isGuarded: true) + case .constructWithSpread(let op): + return ConstructWithSpread(numArguments: op.numArguments, spreads: op.spreads, isGuarded: true) + case .callMethod(let op): + return CallMethod(methodName: op.methodName, numArguments: op.numArguments, isGuarded: true) + case .callMethodWithSpread(let op): + return CallMethodWithSpread(methodName: op.methodName, numArguments: op.numArguments, spreads: op.spreads, isGuarded: true) + case .callComputedMethod(let op): + return CallComputedMethod(numArguments: op.numArguments, isGuarded: true) + case .callComputedMethodWithSpread(let op): + return CallComputedMethodWithSpread(numArguments: op.numArguments, spreads: op.spreads, isGuarded: true) + default: + fatalError("All guardable operations should be handled") + } + } + + // Helper functions to disable guards. + // If the given operation already has guarding disabled, then this function does + // nothing and simply returns the input. Otherwise it creates a copy of the + // operations which has guarding disabled. + static func disableGuard(of operation: GuardableOperation) -> GuardableOperation { + if !operation.isGuarded { + return operation + } + switch operation.opcode { + case .getProperty(let op): + return GetProperty(propertyName: op.propertyName, isGuarded: false) + case .deleteProperty(let op): + return DeleteProperty(propertyName: op.propertyName, isGuarded: false) + case .getElement(let op): + return GetElement(index: op.index, isGuarded: false) + case .deleteElement(let op): + return DeleteElement(index: op.index, isGuarded: false) + case .getComputedProperty: + return GetComputedProperty(isGuarded: false) + case .deleteComputedProperty: + return DeleteComputedProperty(isGuarded: false) + case .callFunction(let op): + return CallFunction(numArguments: op.numArguments, isGuarded: false) + case .callFunctionWithSpread(let op): + return CallFunctionWithSpread(numArguments: op.numArguments, spreads: op.spreads, isGuarded: false) + case .construct(let op): + return Construct(numArguments: op.numArguments, isGuarded: false) + case .constructWithSpread(let op): + return ConstructWithSpread(numArguments: op.numArguments, spreads: op.spreads, isGuarded: false) + case .callMethod(let op): + return CallMethod(methodName: op.methodName, numArguments: op.numArguments, isGuarded: false) + case .callMethodWithSpread(let op): + return CallMethodWithSpread(methodName: op.methodName, numArguments: op.numArguments, spreads: op.spreads, isGuarded: false) + case .callComputedMethod(let op): + return CallComputedMethod(numArguments: op.numArguments, isGuarded: false) + case .callComputedMethodWithSpread(let op): + return CallComputedMethodWithSpread(numArguments: op.numArguments, spreads: op.spreads, isGuarded: false) + default: + fatalError("All guardable operations should be handled") + } + } } final class LoadInteger: JsOperation { diff --git a/Sources/Fuzzilli/Fuzzer.swift b/Sources/Fuzzilli/Fuzzer.swift index 5eb95c021..38b119d3c 100644 --- a/Sources/Fuzzilli/Fuzzer.swift +++ b/Sources/Fuzzilli/Fuzzer.swift @@ -375,6 +375,9 @@ public class Fuzzer { dispatchEvent(event, data: ()) } + /// Tuple containing the result of importing a program. + public typealias ImportResult = (wasImported: Bool, executionOutcome: ExecutionOutcome) + /// Imports a potentially interesting program into this fuzzer. /// /// When importing, the program will be treated like one that was generated by this fuzzer. As such it will @@ -382,39 +385,41 @@ public class Fuzzer { /// When dropout is enabled, a configurable percentage of programs will be ignored during importing. This /// mechanism can help reduce the similarity of different fuzzer instances. @discardableResult - public func importProgram(_ program: Program, enableDropout: Bool = false, origin: ProgramOrigin) -> ExecutionOutcome { + public func importProgram(_ program: Program, origin: ProgramOrigin, enableDropout: Bool = false) -> ImportResult { dispatchPrecondition(condition: .onQueue(queue)) if enableDropout && probability(config.dropoutRate) { - return .succeeded + return ImportResult(wasImported: false, executionOutcome: .succeeded) } let execution = execute(program, purpose: .programImport) + var wasImported = false switch execution.outcome { case .crashed(let termsig): // Here we explicitly deal with the possibility that an interesting sample // from another instance triggers a crash in this instance. processCrash(program, withSignal: termsig, withStderr: execution.stderr, withStdout: execution.stdout, origin: origin, withExectime: execution.execTime) + case .succeeded: - var imported = false if let aspects = evaluator.evaluate(execution) { - imported = processMaybeInteresting(program, havingAspects: aspects, origin: origin) + wasImported = processMaybeInteresting(program, havingAspects: aspects, origin: origin) } - if case .corpusImport(let mode) = origin, mode == .full, !imported { + if case .corpusImport(let mode) = origin, mode == .full, !wasImported { // We're performing a full corpus import, so the sample still needs to be added to our corpus even though it doesn't trigger any new behaviour. corpus.add(program, ProgramAspects(outcome: .succeeded)) // We also dispatch the InterestingProgramFound event here since we technically found an interesting program, but also so that the program is forwarded to child nodes. dispatchEvent(events.InterestingProgramFound, data: (program, origin)) + wasImported = true } default: break } - return execution.outcome + return ImportResult(wasImported: wasImported, executionOutcome: execution.outcome) } /// Imports a crashing program into this fuzzer. @@ -432,6 +437,145 @@ public class Fuzzer { } } + /// Helper function for removing calls to certain functions from a Program. + /// + /// This is useful for example for importing programs that contain function calls to functions that are + /// not available in the fuzzing enviroment. + /// Internally, this function simply replace all uses of the specified functions with calls to a dummy function. + /// We then rely on minimization to remove the actual call instructions (or any other uses). + /// + /// The function names may contain the wildcard character `*`, but _only_ as last character, in which case + /// a prefix match will be performed instead of a string comparison. + /// + /// TODO We could consider moving this function into a "ProgramTransformations" or similar static class if we + /// have other program transformations that could go there as well. + private func removeCallsTo(_ filteredFunctions: [String], from program: Program) -> Program { + func shouldRemoveUsesOf(_ name: String) -> Bool { + for filteredFunction in filteredFunctions { + if filteredFunction.last == "*" { + if name.starts(with: filteredFunction.dropLast()) { + return true + } + } else { + assert(!filteredFunction.contains("*")) + if name == filteredFunction { + return true + } + } + } + return false + } + + let b = makeBuilder() + + let dummy = b.buildPlainFunction(with: .parameters(n: 0)) { _ in } + var variablesToReplaceWithDummy = VariableSet() + b.adopting(from: program) { + for instr in program.code { + var removeInstruction = false + switch instr.op.opcode { + case .loadNamedVariable(let op): + if shouldRemoveUsesOf(op.variableName) { + removeInstruction = true + variablesToReplaceWithDummy.insert(instr.output) + } + case .loadBuiltin(let op): + // We expect builtins to always be available and don't want to filter them out. + assert(!shouldRemoveUsesOf(op.builtinName)) + default: + break + } + + if !removeInstruction { + let inouts = instr.inouts.map({ variablesToReplaceWithDummy.contains($0) ? dummy : b.adopt($0) }) + let newInstr = Instruction(instr.op, inouts: inouts, flags: instr.flags) + b.append(newInstr) + } + } + } + + let foundAnyFunctionsToRemove = !variablesToReplaceWithDummy.isEmpty + if foundAnyFunctionsToRemove { + return b.finalize() + } else { + // Just return the original program to avoid adding a dummy function when it isn't needed + return program + } + } + + /// Imports and potentially modifies a program into this fuzzer + /// + /// For the most part, this is similar to `importProgram`. However, if the imported program fails to execute + /// (e.g. because it throws a runtime exception), then this function will try to "fix" the program so that it + /// executes successfully and can be imported. + private static let maxProgramImportFixupAttempts = 3 + public func importProgramWithFixup(_ originalProgram: Program, origin: ProgramOrigin) -> (result: ImportResult, fixupAttempts: Int) { + var program = originalProgram + var result = importProgram(program, origin: origin) + + // Only attempt fixup if the program failed to execute successfully. In particular, ignore timeouts and + // crashes here, but also take into account that not all successfully executing programs will be imported. + if !result.executionOutcome.isFailure() { + return (result, 0) + } + assert(!result.wasImported) + + let b = makeBuilder() + + // First attempt at fixing the program: remove known test functions from the program which are + // available in the unit test environment but not in the default environment. + let filteredFunctions = [ + // Functions used in V8's test suite + "assert*", + "print*", + // Functions used in Mozilla's test suite + "startTest", + "enterFunc", + "exitFunc", + "report*", + "options*", + ] + program = removeCallsTo(filteredFunctions, from: program) + result = importProgram(program, origin: origin) + if !result.executionOutcome.isFailure() { + return (result, 1) + } + assert(!result.wasImported) + + // Second attempt at fixing the program: enable guards (try-catch) for all guardable operations, then + // remove all guards that aren't needed (because no exception is thrown). + for instr in program.code { + var newOp = instr.op + if let op = instr.op as? GuardableOperation { + newOp = GuardableOperation.enableGuard(of: op) + } + b.append(Instruction(newOp, inouts: instr.inouts, flags: instr.flags)) + } + program = b.finalize() + if let result = currentCorpusImportJob.fixupMutator.mutate(program, for: self) { + program = result + } + result = importProgram(program, origin: origin) + if !result.executionOutcome.isFailure() { + return (result, 2) + } + assert(!result.wasImported) + + // Third and final attempt at fixing up the program: simply wrap the entire program in a try-catch block. + b.buildTryCatchFinally(tryBody: { + b.adopting(from: program) { + for instr in program.code { + b.adopt(instr) + } + } + }, catchBody: { _ in }) + program = b.finalize() + + result = importProgram(program, origin: origin) + assert(Fuzzer.maxProgramImportFixupAttempts == 3) + return (result, 3) + } + /// Schedules the given corpus of programs to be imported into this fuzzer. /// /// Corpus import happens asynchronously as it may take a considerable amount of time (each program @@ -641,39 +785,32 @@ public class Fuzzer { case .corpusImport: assert(!currentCorpusImportJob.isFinished) - var program = currentCorpusImportJob.nextProgram() + let program = currentCorpusImportJob.nextProgram() - if currentCorpusImportJob.numberOfProgramsImportedSoFar % 500 == 0 { - logger.info("Corpus import progress: imported \(currentCorpusImportJob.numberOfProgramsImportedSoFar) of \(currentCorpusImportJob.totalNumberOfProgramsToImport) programs") + if currentCorpusImportJob.numberOfProgramsProcessedSoFar % 500 == 0 { + logger.info("Corpus import progress: processed \(currentCorpusImportJob.numberOfProgramsProcessedSoFar) of \(currentCorpusImportJob.totalNumberOfProgramsToImport) programs") } - var outcome = importProgram(program, origin: .corpusImport(mode: currentCorpusImportJob.importMode)) - if case .failed = outcome { - // Wrap the entire program in a try-catch block and retry. - // TODO we could be a lot smarter here. For example, we could instrument the program to determine - // exactly which instruction causes the exception to be thrown, and then either delete that - // instruction or wrap just that instruction in try-catch. - let b = makeBuilder() - b.buildTryCatchFinally(tryBody: { - b.adopting(from: program) { - for instr in program.code { - b.adopt(instr) - } - } - }, catchBody: { _ in }) - program = b.finalize() - currentCorpusImportJob.notifyProgramNeededFixup() - outcome = importProgram(program, origin: .corpusImport(mode: currentCorpusImportJob.importMode)) - } - currentCorpusImportJob.notifyImportOutcome(outcome) + let (result, fixupAttempts) = importProgramWithFixup(program, origin: .corpusImport(mode: currentCorpusImportJob.importMode)) + currentCorpusImportJob.notifyImportOutcome(result, fixupAttempts: fixupAttempts) if currentCorpusImportJob.isFinished { logger.info("Corpus import finished:") - logger.info("\(currentCorpusImportJob.numberOfProgramsThatExecutedSuccessfullyDuringImport)/\(currentCorpusImportJob.totalNumberOfProgramsToImport) programs executed successfully during import") - logger.info("\(currentCorpusImportJob.numberOfProgramsThatNeededFixup)/\(currentCorpusImportJob.totalNumberOfProgramsToImport) programs needed fixup during import (wrapping in try-catch)") - logger.info("\(currentCorpusImportJob.numberOfProgramsThatTimedOutDuringImport)/\(currentCorpusImportJob.totalNumberOfProgramsToImport) programs timed out during import") - logger.info("\(currentCorpusImportJob.numberOfProgramsThatFailedDuringImport)/\(currentCorpusImportJob.totalNumberOfProgramsToImport) programs failed to execute during import") - logger.info("Corpus now contains \(corpus.size) programs") + logger.info("\(currentCorpusImportJob.numberOfProgramsThatExecutedSuccessfullyDuringImport)/\(currentCorpusImportJob.totalNumberOfProgramsToImport) programs executed successfully") + logger.info(" Of which \(currentCorpusImportJob.numberOfProgramsThatWereImport) programs were added to the corpus") + logger.info("\(currentCorpusImportJob.numberOfProgramsThatNeededFixup)/\(currentCorpusImportJob.totalNumberOfProgramsToImport) programs needed fixup during import") + logger.info(" \(currentCorpusImportJob.numberOfProgramsThatNeededOneFixupAttempt) succeeded after attempt 1") + logger.info(" \(currentCorpusImportJob.numberOfProgramsThatNeededTwoFixupAttempts) succeeded after attempt 2") + logger.info(" \(currentCorpusImportJob.numberOfProgramsThatNeededThreeFixupAttempts) succeeded after attempt 3") + logger.info("\(currentCorpusImportJob.numberOfProgramsThatFailedDuringImport)/\(currentCorpusImportJob.totalNumberOfProgramsToImport) programs failed to execute (even after fixup) and weren't imported") + logger.info("\(currentCorpusImportJob.numberOfProgramsThatTimedOutDuringImport)/\(currentCorpusImportJob.totalNumberOfProgramsToImport) programs timed out and weren't imported") + + let successRatio = Double(currentCorpusImportJob.numberOfProgramsThatExecutedSuccessfullyDuringImport) / Double(currentCorpusImportJob.totalNumberOfProgramsToImport) + let failureRatio = 1.0 - successRatio + if failureRatio >= 0.25 { + logger.warning("\(String(format: "%.2f", failureRatio * 100))% of imported programs failed to execute successfully and therefore couldn't be imported.") + } + dispatchEvent(events.CorpusImportComplete) changeState(to: .fuzzing) } @@ -813,11 +950,22 @@ public class Fuzzer { let importMode: CorpusImportMode let totalNumberOfProgramsToImport: Int - private(set) var numberOfProgramsImportedSoFar = 0 + // We use a fixup mutator for fixing imported programs that throw an exception. + let fixupMutator = FixupMutator(name: "CorpusImportFixupMutator") + + private(set) var numberOfProgramsProcessedSoFar = 0 + private(set) var numberOfProgramsThatExecutedSuccessfullyDuringImport = 0 + private(set) var numberOfProgramsThatWereImport = 0 private(set) var numberOfProgramsThatFailedDuringImport = 0 private(set) var numberOfProgramsThatTimedOutDuringImport = 0 - private(set) var numberOfProgramsThatExecutedSuccessfullyDuringImport = 0 - private(set) var numberOfProgramsThatNeededFixup = 0 + private(set) var numberOfProgramsThatNeededOneFixupAttempt = 0 + private(set) var numberOfProgramsThatNeededTwoFixupAttempts = 0 + private(set) var numberOfProgramsThatNeededThreeFixupAttempts = 0 + + var numberOfProgramsThatNeededFixup: Int { + assert(Fuzzer.maxProgramImportFixupAttempts == 3) + return numberOfProgramsThatNeededOneFixupAttempt + numberOfProgramsThatNeededTwoFixupAttempts + numberOfProgramsThatNeededThreeFixupAttempts + } init(corpus: [Program], mode: CorpusImportMode) { self.corpusToImport = corpus.reversed() // Programs are taken from the end. @@ -831,32 +979,41 @@ public class Fuzzer { mutating func nextProgram() -> Program { assert(!isFinished) - numberOfProgramsImportedSoFar += 1 + numberOfProgramsProcessedSoFar += 1 return corpusToImport.removeLast() } - mutating func notifyImportOutcome(_ outcome: ExecutionOutcome) { - switch outcome { + mutating func notifyImportOutcome(_ result: ImportResult, fixupAttempts: Int) { + switch result.executionOutcome { case .crashed: + assert(!result.wasImported) // This is unexpected so we don't track these break case .failed: + assert(!result.wasImported) numberOfProgramsThatFailedDuringImport += 1 case .succeeded: numberOfProgramsThatExecutedSuccessfullyDuringImport += 1 + if result.wasImported { + numberOfProgramsThatWereImport += 1 + } + switch fixupAttempts { + case 0: break + case 1: numberOfProgramsThatNeededOneFixupAttempt += 1 + case 2: numberOfProgramsThatNeededTwoFixupAttempts += 1 + case 3: numberOfProgramsThatNeededThreeFixupAttempts += 1 + default: fatalError("Unexpected number of fixup rounds: \(fixupAttempts)") + } case .timedOut: + assert(!result.wasImported) numberOfProgramsThatTimedOutDuringImport += 1 } } - mutating func notifyProgramNeededFixup() { - numberOfProgramsThatNeededFixup += 1 - } - func progress() -> Double { let numberOfProgramsToImport = Double(totalNumberOfProgramsToImport) - let numberOfProgramsAlreadyImported = Double(numberOfProgramsImportedSoFar) - return numberOfProgramsAlreadyImported / numberOfProgramsToImport + let numberOfProgramsAlreadyProcessed = Double(numberOfProgramsProcessedSoFar) + return numberOfProgramsAlreadyProcessed / numberOfProgramsToImport } } } diff --git a/Sources/Fuzzilli/Minimization/SimplifyingReducer.swift b/Sources/Fuzzilli/Minimization/SimplifyingReducer.swift index 600e0e980..4b7e7305f 100644 --- a/Sources/Fuzzilli/Minimization/SimplifyingReducer.swift +++ b/Sources/Fuzzilli/Minimization/SimplifyingReducer.swift @@ -97,72 +97,10 @@ struct SimplifyingReducer: Reducer { // This will attempt to turn guarded operations into unguarded ones. // In the lifted JavaScript code, this would turn something like `try { o.foo(); } catch (e) {}` into `o.foo();` for instr in helper.code { - var newOp: Operation? = nil - switch instr.op.opcode { - case .getProperty(let op): - if op.isGuarded { - newOp = GetProperty(propertyName: op.propertyName, isGuarded: false) - } - case .deleteProperty(let op): - if op.isGuarded { - newOp = DeleteProperty(propertyName: op.propertyName, isGuarded: false) - } - case .getElement(let op): - if op.isGuarded { - newOp = GetElement(index: op.index, isGuarded: false) - } - case .deleteElement(let op): - if op.isGuarded { - newOp = DeleteElement(index: op.index, isGuarded: false) - } - case .getComputedProperty(let op): - if op.isGuarded { - newOp = GetComputedProperty(isGuarded: false) - } - case .deleteComputedProperty(let op): - if op.isGuarded { - newOp = DeleteComputedProperty(isGuarded: false) - } - case .callFunction(let op): - if op.isGuarded { - newOp = CallFunction(numArguments: op.numArguments, isGuarded: false) - } - case .callFunctionWithSpread(let op): - if op.isGuarded { - newOp = CallFunctionWithSpread(numArguments: op.numArguments, spreads: op.spreads, isGuarded: false) - } - case .construct(let op): - if op.isGuarded { - newOp = Construct(numArguments: op.numArguments, isGuarded: false) - } - case .constructWithSpread(let op): - if op.isGuarded { - newOp = ConstructWithSpread(numArguments: op.numArguments, spreads: op.spreads, isGuarded: false) - } - case .callMethod(let op): - if op.isGuarded { - newOp = CallMethod(methodName: op.methodName, numArguments: op.numArguments, isGuarded: false) - } - case .callMethodWithSpread(let op): - if op.isGuarded { - newOp = CallMethodWithSpread(methodName: op.methodName, numArguments: op.numArguments, spreads: op.spreads, isGuarded: false) - } - case .callComputedMethod(let op): - if op.isGuarded { - newOp = CallComputedMethod(numArguments: op.numArguments, isGuarded: false) - } - case .callComputedMethodWithSpread(let op): - if op.isGuarded { - newOp = CallComputedMethodWithSpread(numArguments: op.numArguments, spreads: op.spreads, isGuarded: false) - } - - default: - assert(!(instr.op is GuardableOperation), "All guardable operations should be covered") - break - } - - if let op = newOp { - helper.tryReplacing(instructionAt: instr.index, with: Instruction(op, inouts: instr.inouts, flags: .empty)) + guard let op = instr.op as? GuardableOperation else { continue } + let newOp = GuardableOperation.disableGuard(of: op) + if newOp !== op { + helper.tryReplacing(instructionAt: instr.index, with: Instruction(newOp, inouts: instr.inouts, flags: .empty)) } } } diff --git a/Sources/Fuzzilli/Modules/Sync.swift b/Sources/Fuzzilli/Modules/Sync.swift index 608ebae65..d97c99ca7 100644 --- a/Sources/Fuzzilli/Modules/Sync.swift +++ b/Sources/Fuzzilli/Modules/Sync.swift @@ -252,7 +252,7 @@ public class DistributedFuzzingParentNode: DistributedFuzzingNode, Module { do { let proto = try Fuzzilli_Protobuf_Program(serializedBytes: data) let program = try Program(from: proto) - fuzzer.importProgram(program, enableDropout: false, origin: .child(id: child)) + fuzzer.importProgram(program, origin: .child(id: child), enableDropout: false) } catch { logger.warning("Received malformed program from child node: \(error)") } @@ -451,10 +451,10 @@ public class DistributedFuzzingChildNode: DistributedFuzzingNode, Module { // Regardless of the corpus import mode used by the parent node, as a child node we // always add the program to our corpus without further checks or minimization as // that will, if necessary, already have been performed by our parent node. - fuzzer.importProgram(program, enableDropout: true, origin: .corpusImport(mode: .full)) + fuzzer.importProgram(program, origin: .corpusImport(mode: .full), enableDropout: true) } else { assert(messageType == .interestingProgram) - fuzzer.importProgram(program, enableDropout: true, origin: .parent) + fuzzer.importProgram(program, origin: .parent, enableDropout: true) } } catch { logger.warning("Received malformed program") diff --git a/Sources/Fuzzilli/Mutators/FixupMutator.swift b/Sources/Fuzzilli/Mutators/FixupMutator.swift index 1a8fd7d23..6b8f27919 100644 --- a/Sources/Fuzzilli/Mutators/FixupMutator.swift +++ b/Sources/Fuzzilli/Mutators/FixupMutator.swift @@ -99,7 +99,7 @@ public class FixupMutator: RuntimeAssistedMutator { fixup(instr, performing: op, guarded: guarded, withInputs: inputs, with: b) } - func fixupIfUnguarded(_ instr: Instruction, performing op: ActionOperation, guarded: Bool, withInputs inputs: [Action.Input], with b: ProgramBuilder) { + func fixupIfGuarded(_ instr: Instruction, performing op: ActionOperation, guarded: Bool, withInputs inputs: [Action.Input], with b: ProgramBuilder) { guard guarded else { b.append(instr) return @@ -116,11 +116,11 @@ public class FixupMutator: RuntimeAssistedMutator { // exception would be raised. case .callFunction(let op): let inputs = (0..