Skip to content

Commit

Permalink
Add function to recursively generate arguments
Browse files Browse the repository at this point in the history
of the right type for a given Signature. This can be useful if we want
to construct objects that have more complicated argument constraints
  • Loading branch information
carl-smith committed Jul 25, 2024
1 parent 139d074 commit d5785d4
Show file tree
Hide file tree
Showing 8 changed files with 277 additions and 3 deletions.
137 changes: 137 additions & 0 deletions Sources/Fuzzilli/Base/ProgramBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
3 changes: 3 additions & 0 deletions Sources/Fuzzilli/CodeGen/CodeGeneratorWeights.swift
Original file line number Diff line number Diff line change
Expand Up @@ -188,4 +188,7 @@ public let codeGeneratorWeights = [
"IteratorGenerator": 5,
"ConstructWithDifferentNewTargetGenerator": 5,
"ObjectHierarchyGenerator": 10,
"ApiConstructorCallGenerator": 15,
"ApiMethodCallGenerator": 15,
"ApiFunctionCallGenerator": 15,
]
31 changes: 31 additions & 0 deletions Sources/Fuzzilli/CodeGen/CodeGenerators.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 })
}
]

Expand Down
8 changes: 8 additions & 0 deletions Sources/Fuzzilli/Environment/Environment.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
34 changes: 34 additions & 0 deletions Sources/Fuzzilli/Environment/JavaScriptEnvironment.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)")
Expand All @@ -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
Expand Down
10 changes: 7 additions & 3 deletions Sources/Fuzzilli/FuzzIL/TypeSystem.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
}
Expand Down Expand Up @@ -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.
Expand Down
8 changes: 8 additions & 0 deletions Sources/Fuzzilli/Util/MockFuzzer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
49 changes: 49 additions & 0 deletions Tests/FuzzilliTests/ProgramBuilderTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}

0 comments on commit d5785d4

Please sign in to comment.