diff --git a/Sources/Fuzzilli/Base/ProgramBuilder.swift b/Sources/Fuzzilli/Base/ProgramBuilder.swift index c1251b88f..98e0f401b 100644 --- a/Sources/Fuzzilli/Base/ProgramBuilder.swift +++ b/Sources/Fuzzilli/Base/ProgramBuilder.swift @@ -86,6 +86,14 @@ public class ProgramBuilder { /// Type inference for JavaScript variables. private var jsTyper: JSTyper + /// Argument generation budget. + /// This budget is used in `findOrGenerateArguments(forSignature)` and tracks the upper limit of variables that that function should emit. + /// If that upper limit is reached the function will stop generating new variables and use existing ones instead. + /// If this value is set to nil, there is no argument generation happening, every argument generation should enter the recursive function (findOrGenerateArgumentsInternal) through the public non-internal one. + private var argumentGenerationVariableBudget: Int? = nil + /// This is the top most signature that was requested when `findOrGeneratorArguments(forSignature)` was called, this is helpful for debugging. + private var argumentGenerationSignature: Signature? = nil + /// Stack of active object literals. /// /// This needs to be a stack as object literals can be nested, for example if an object @@ -464,6 +472,135 @@ public class ProgramBuilder { return .parameters(params) } + public func findOrGenerateArguments(forSignature signature: Signature, maxNumberOfVariablesToGenerate: Int = 100) -> [Variable] { + assert(context.contains(.javascript)) + + argumentGenerationVariableBudget = numVariables + maxNumberOfVariablesToGenerate + argumentGenerationSignature = signature + + defer { + argumentGenerationVariableBudget = nil + argumentGenerationSignature = nil + } + + return findOrGenerateArgumentsInternal(forSignature: signature) + } + + private func findOrGenerateArgumentsInternal(forSignature: Signature) -> [Variable] { + + var args: [Variable] = [] + + // This should be called whenever we have a type that has known information about its properties but we don't have a constructor for it. + // This can be the case for configuration objects, e.g. objects that can be passed into DOMAPIs. + func createObjectWithProperties(_ type: ILType) -> Variable { + assert(type.MayBe(.object())) + + var properties: [String: Variable] = [:] + + for propertyName in type.properties { + // If we have an object that has a group, we should get a type here, otherwise if we don't have a group, we will get .anything. + let propType = fuzzer.environment.type(ofProperty: propertyName, on: type) + // Here we can enter generateType again, and end up here again if we need config objects for config objects, therefore we pass the recursion counter back into generateType, which will bail out eventually if there is a cycle. + properties[propertyName] = generateType(propType) + } + + return createObject(with: properties) + } + + func createObjectWithGroup(type: ILType) -> Variable { + let group = type.group! + + // We can be sure that we have such a builtin with a signature because the Environment checks this during initialization. + let signature = fuzzer.environment.type(ofBuiltin: group).signature! + let constructor = loadBuiltin(group) + let arguments = findOrGenerateArgumentsInternal(forSignature: signature) + let constructed = construct(constructor, withArgs: arguments) + + return constructed + } + + func generateType(_ type: ILType) -> Variable { + if probability(0.5) { + if let existingVariable = randomVariable(ofType: type) { + return existingVariable + } + } + + if numVariables > argumentGenerationVariableBudget! { + logger.warning("Reached variable generation limit in generateType for Signature: \(argumentGenerationSignature!).") + return randomVariable(forUseAs: type) + } + + // We only need to check against all base types from TypeSystem.swift, this works because we use .MayBe + // TODO: Not sure how we should handle merge types, e.g. .string + .object(...). + let typeGenerators: [(ILType, () -> Variable)] = [ + (.integer, { return self.loadInt(self.randomInt()) }), + (.string, { return self.loadString(self.randomString()) }), + (.boolean, { return self.loadBool(probability(0.5)) }), + (.bigint, { return self.loadBigInt(self.randomInt()) }), + (.float, { return self.loadFloat(self.randomFloat()) }), + (.regexp, { + let (pattern, flags) = self.randomRegExpPatternAndFlags() + return self.loadRegExp(pattern, flags) + }), + (.iterable, { return self.createArray(with: self.randomVariables(upTo: 5)) }), + (.function(), { + // TODO: We could technically generate a full function here but then we would enter the full code generation logic which could do anything. + // Because we want to avoid this, we will just pick anything that can be a function. + return self.randomVariable(forUseAs: .function()) + }), + (.undefined, { return self.loadUndefined() }), + (.constructor(), { + // TODO: We have the same issue as above for functions. + return self.randomVariable(forUseAs: .constructor()) + }), + (.object(), { + // If we have a group on this object and we have a builtin, that means we can construct it with new. + if let group = type.group, self.fuzzer.environment.hasBuiltin(group) && self.fuzzer.environment.type(ofBuiltin: group).Is(.constructor()) { + return createObjectWithGroup(type: type) + } else { + // Otherwise this is one of the following: + // 1. an object with more type information, i.e. it has a group, but no associated builtin, e.g. we cannot construct it with new. + // 2. an object without a group, but it has some required fields. + // In either case, we try to construct such an object. + return createObjectWithProperties(type) + } + }) + ] + + // Make sure that we walk over these tests and their generators randomly. + // The requested type could be a Union of other types and as such we want to randomly generate one of them, therefore we also use the MayBe test below. + for (t, generate) in typeGenerators.shuffled() { + if type.MayBe(t) { + let variable = generate() + return variable + } + } + + logger.warning("Type \(type) was not handled, returning random variable.") + return randomVariable(forUseAs: type) + } + + outer: for parameter in forSignature.parameters { + switch parameter { + case .plain(let t): + args.append(generateType(t)) + case .opt(let t): + if probability(0.5) { + args.append(generateType(t)) + } else { + // We decided to not provide an optional parameter, so we can stop here. + break outer + } + case .rest(let t): + for _ in 0...Int.random(in: 1...3) { + args.append(generateType(t)) + } + } + } + + return args + } /// /// Access to variables. diff --git a/Sources/Fuzzilli/CodeGen/CodeGeneratorWeights.swift b/Sources/Fuzzilli/CodeGen/CodeGeneratorWeights.swift index 8699e2cc2..bddfbc5e1 100644 --- a/Sources/Fuzzilli/CodeGen/CodeGeneratorWeights.swift +++ b/Sources/Fuzzilli/CodeGen/CodeGeneratorWeights.swift @@ -188,4 +188,7 @@ public let codeGeneratorWeights = [ "IteratorGenerator": 5, "ConstructWithDifferentNewTargetGenerator": 5, "ObjectHierarchyGenerator": 10, + "ApiConstructorCallGenerator": 15, + "ApiMethodCallGenerator": 15, + "ApiFunctionCallGenerator": 15, ] diff --git a/Sources/Fuzzilli/CodeGen/CodeGenerators.swift b/Sources/Fuzzilli/CodeGen/CodeGenerators.swift index 45684dc8b..791362c41 100644 --- a/Sources/Fuzzilli/CodeGen/CodeGenerators.swift +++ b/Sources/Fuzzilli/CodeGen/CodeGenerators.swift @@ -1795,6 +1795,37 @@ public let CodeGenerators: [CodeGenerator] = [ CodeGenerator("LoadNewTargetGenerator", inContext: .subroutine) { b in assert(b.context.contains(.subroutine)) b.loadNewTarget() + }, + + // TODO: think about merging this with the regular ConstructorCallGenerator. + CodeGenerator("ApiConstructorCallGenerator", inputs: .required(.constructor())) { b, c in + let signature = b.type(of: c).signature ?? Signature.forUnknownFunction + + b.buildTryCatchFinally(tryBody: { + let args = b.findOrGenerateArguments(forSignature: signature) + b.construct(c, withArgs: args) + }, catchBody: { _ in }) + }, + + // TODO: think about merging this with the regular MethodCallGenerator. + CodeGenerator("ApiMethodCallGenerator", inputs: .required(.object())) { b, o in + let methodName = b.type(of: o).randomMethod() ?? b.randomMethodName() + + let signature = b.methodSignature(of: methodName, on: o) + + b.buildTryCatchFinally(tryBody: { + let args = b.findOrGenerateArguments(forSignature: signature) + b.callMethod(methodName, on: o, withArgs: args) + }, catchBody: { _ in }) + }, + + CodeGenerator("ApiFunctionCallGenerator", inputs: .required(.function())) { b, f in + let signature = b.type(of: f).signature ?? Signature.forUnknownFunction + + b.buildTryCatchFinally(tryBody: { + let args = b.findOrGenerateArguments(forSignature: signature) + b.callFunction(f, withArgs: args) + }, catchBody: { _ in }) } ] diff --git a/Sources/Fuzzilli/Environment/Environment.swift b/Sources/Fuzzilli/Environment/Environment.swift index 5f43c390a..88cf0608c 100644 --- a/Sources/Fuzzilli/Environment/Environment.swift +++ b/Sources/Fuzzilli/Environment/Environment.swift @@ -82,6 +82,14 @@ public protocol Environment: Component { var promiseType: ILType { get } + /// Returns true if the given type is a builtin, e.g. can be constructed + /// This is useful for types that are dictionary config objects, as we will have an object group for them, i.e. type(ofProperty, on) will work but type(ofBuiltin) won't + func hasBuiltin(_ name: String) -> Bool + + /// Returns true if we have an object group associated with this name + /// config objects have a group but no constructor, i.e. loadable builtin associated + func hasGroup(_ name: String) -> Bool + /// Retuns the type of the builtin with the given name. func type(ofBuiltin builtinName: String) -> ILType diff --git a/Sources/Fuzzilli/Environment/JavaScriptEnvironment.swift b/Sources/Fuzzilli/Environment/JavaScriptEnvironment.swift index 5eeaafae6..182d96bf4 100644 --- a/Sources/Fuzzilli/Environment/JavaScriptEnvironment.swift +++ b/Sources/Fuzzilli/Environment/JavaScriptEnvironment.swift @@ -427,6 +427,8 @@ public class JavaScriptEnvironment: ComponentBase, Environment { assert(builtinMethods.contains("valueOf")) assert(builtinMethods.contains("toString")) + checkConstructorAvailability() + // Log detailed information about the environment here so users are aware of it and can modify things if they like. logger.info("Initialized static JS environment model") logger.info("Have \(builtins.count) available builtins: \(builtins)") @@ -437,6 +439,38 @@ public class JavaScriptEnvironment: ComponentBase, Environment { logger.info("Have \(customMethods.count) custom method names: \(customMethods)") } + func checkConstructorAvailability() { + logger.info("Checking constructor availability...") + // These constructors return types that are well-known instead of .object types. + let knownExceptions = [ + "Boolean", // returns .boolean + "Number", // returns .number + "Object", // returns plain .object + "Proxy", // returns .anything + ] + for builtin in builtins where type(ofBuiltin: builtin).Is(.constructor()) { + if knownExceptions.contains(builtin) { continue } + if !hasGroup(builtin) { logger.warning("Missing group info for constructable \(builtin)")} + if type(ofBuiltin: builtin).signature == nil { + logger.warning("Missing signature for builtin \(builtin)") + } else { + if !type(ofBuiltin: builtin).signature!.outputType.Is(.object(ofGroup: builtin)) { + logger.warning("Signature for builtin \(builtin) is mismatching") + } + } + + } + logger.info("Done checking constructor availability...") + } + + public func hasBuiltin(_ name: String) -> Bool { + return self.builtinTypes.keys.contains(name) + } + + public func hasGroup(_ name: String) -> Bool { + return self.groups.keys.contains(name) + } + public func registerObjectGroup(_ group: ObjectGroup) { assert(groups[group.name] == nil) groups[group.name] = group diff --git a/Sources/Fuzzilli/FuzzIL/TypeSystem.swift b/Sources/Fuzzilli/FuzzIL/TypeSystem.swift index e80c12cfe..567a15a56 100644 --- a/Sources/Fuzzilli/FuzzIL/TypeSystem.swift +++ b/Sources/Fuzzilli/FuzzIL/TypeSystem.swift @@ -874,12 +874,15 @@ extension ParameterList { var sawOptionals = false for (i, p) in self.enumerated() { switch p { - case .rest(_): + case .rest(let t): + assert(!t.Is(.nothing)) // Only the last parameter can be a rest parameter. guard i == count - 1 else { return false } - case .opt(_): + case .opt(let t): + assert(!t.Is(.nothing)) sawOptionals = true - case .plain(_): + case .plain(let t): + assert(!t.Is(.nothing)) // Optional parameters must not be followed by regular parameters. guard !sawOptionals else { return false } } @@ -921,6 +924,7 @@ public struct Signature: Hashable, CustomStringConvertible { assert(parameters.areValid()) self.parameters = parameters self.outputType = returnType + assert(!outputType.Is(.nothing)) } // Constructs a function with N parameters of any type and returning .anything. diff --git a/Sources/Fuzzilli/Util/MockFuzzer.swift b/Sources/Fuzzilli/Util/MockFuzzer.swift index be921c972..877ff9b80 100644 --- a/Sources/Fuzzilli/Util/MockFuzzer.swift +++ b/Sources/Fuzzilli/Util/MockFuzzer.swift @@ -73,6 +73,14 @@ class MockEnvironment: ComponentBase, Environment { return .anything } + func hasBuiltin(_ name: String) -> Bool { + return builtinTypes.keys.contains(name) + } + + func hasGroup(_ name: String) -> Bool { + return propertiesByGroup.keys.contains(name) + } + func type(ofBuiltin builtinName: String) -> ILType { return builtinTypes[builtinName] ?? .anything } diff --git a/Tests/FuzzilliTests/ProgramBuilderTest.swift b/Tests/FuzzilliTests/ProgramBuilderTest.swift index 03055d4aa..de597f166 100644 --- a/Tests/FuzzilliTests/ProgramBuilderTest.swift +++ b/Tests/FuzzilliTests/ProgramBuilderTest.swift @@ -2396,4 +2396,53 @@ class ProgramBuilderTests: XCTestCase { let result = b.finalize() XCTAssert(result.code.contains(where: { $0.op is BeginSwitchCase })) } + + func testArgumentGenerationForKnownSignature() { + let env = JavaScriptEnvironment() + let fuzzer = makeMockFuzzer(environment: env) + let b = fuzzer.makeBuilder() + + b.loadInt(42) + + let constructor = b.loadBuiltin("DataView") + let signature = env.type(ofBuiltin: "DataView").signature! + + let variables = b.findOrGenerateArguments(forSignature: signature) + + XCTAssertTrue(b.type(of: variables[0]).Is(.object(ofGroup: "ArrayBuffer"))) + if (variables.count > 1) { + XCTAssertTrue(b.type(of: variables[1]).Is(.number)) + } + + b.construct(constructor, withArgs: variables) + } + + func testArgumentGenerationForKnownSignatureWithLimit() { + let env = JavaScriptEnvironment() + let fuzzer = makeMockFuzzer(environment: env) + let b = fuzzer.makeBuilder() + + b.loadInt(42) + + let typeA: ILType = .object(withProperties: ["a", "b"]) + let typeB: ILType = .object(withProperties: ["c", "d"]) + + let signature: Signature = [.plain(typeA), .plain(typeB)] => .undefined + + var args = b.findOrGenerateArguments(forSignature: signature) + XCTAssertEqual(args.count, 2) + + // check that args have the right types + XCTAssert(b.type(of: args[0]).Is(typeA)) + XCTAssert(b.type(of: args[1]).Is(typeB)) + + let previous = b.numberOfVisibleVariables + + args = b.findOrGenerateArguments(forSignature: signature, maxNumberOfVariablesToGenerate: 1) + XCTAssertEqual(args.count, 2) + + // Ensure first object has the right type, and that we only generated one more variable + XCTAssert(b.type(of: args[0]).Is(typeA)) + XCTAssertEqual(b.numberOfVisibleVariables, previous + 1) + } }