Skip to content

Commit

Permalink
Make single explore operations deterministic.
Browse files Browse the repository at this point in the history
Add a JS RNG such that we can seed explore operations.
If Fuzzilli is unable to convert explore operations into concrete
operations when trying to reproduce a crash, the explore code itself
might be important. In such cases we still want this explore operation
to behave the same way it did when we observed the crash.
  • Loading branch information
carl-smith committed Oct 16, 2023
1 parent 475ea82 commit 7ccd3e8
Show file tree
Hide file tree
Showing 9 changed files with 72 additions and 17 deletions.
3 changes: 2 additions & 1 deletion Sources/Fuzzilli/Base/ProgramBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1874,7 +1874,8 @@ public class ProgramBuilder {
}

public func explore(_ v: Variable, id: String, withArgs arguments: [Variable]) {
emit(Explore(id: id, numArguments: arguments.count), withInputs: [v] + arguments)
let rngSeed = UInt32(truncatingIfNeeded: randomInt())
emit(Explore(id: id, numArguments: arguments.count, rngSeed: rngSeed), withInputs: [v] + arguments)
}

public func probe(_ v: Variable, id: String) {
Expand Down
7 changes: 5 additions & 2 deletions Sources/Fuzzilli/FuzzIL/Instruction.swift
Original file line number Diff line number Diff line change
Expand Up @@ -714,7 +714,10 @@ extension Instruction: ProtobufConvertible {
$0.op = convertEnum(op.op, BinaryOperator.allCases)
}
case .explore(let op):
$0.explore = Fuzzilli_Protobuf_Explore.with { $0.id = op.id }
$0.explore = Fuzzilli_Protobuf_Explore.with {
$0.id = op.id
$0.rngSeed = Int64(op.rngSeed)
}
case .probe(let op):
$0.probe = Fuzzilli_Protobuf_Probe.with { $0.id = op.id }
case .fixup(let op):
Expand Down Expand Up @@ -1137,7 +1140,7 @@ extension Instruction: ProtobufConvertible {
case .updateSuperProperty(let p):
op = UpdateSuperProperty(propertyName: p.propertyName, operator: try convertEnum(p.op, BinaryOperator.allCases))
case .explore(let p):
op = Explore(id: p.id, numArguments: inouts.count - 1)
op = Explore(id: p.id, numArguments: inouts.count - 1, rngSeed: UInt32(p.rngSeed))
case .probe(let p):
op = Probe(id: p.id)
case .fixup(let p):
Expand Down
5 changes: 4 additions & 1 deletion Sources/Fuzzilli/FuzzIL/JsOperations.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2249,12 +2249,15 @@ final class Explore: JsInternalOperation {
override var opcode: Opcode { .explore(self) }

let id: String
// This makes a single explore operation deterministic by seeding a JS RNG
let rngSeed: UInt32

init(id: String, numArguments: Int) {
init(id: String, numArguments: Int, rngSeed: UInt32) {
// IDs should be valid JavaScript property names since they will typically be used in that way.
assert(id.allSatisfy({ $0.isASCII && ($0.isLetter || $0.isNumber) }) && id.contains(where: { $0.isLetter }))

self.id = id
self.rngSeed = rngSeed
super.init(numInputs: numArguments + 1)
}
}
Expand Down
8 changes: 5 additions & 3 deletions Sources/Fuzzilli/Lifting/JavaScriptExploreLifting.swift
Original file line number Diff line number Diff line change
Expand Up @@ -432,7 +432,9 @@ struct JavaScriptExploreLifting {
//
// Exploration entrypoint.
//
function explore(id, v, currentThis, args) {
function explore(id, v, currentThis, args, rngSeed) {
rng.reseed(rngSeed);
// The given arguments may be used as inputs for the action.
if (isUndefined(args) || args.length < 1) throw "Exploration requires at least one additional argument";
Expand Down Expand Up @@ -466,9 +468,9 @@ struct JavaScriptExploreLifting {
currentlyExploring = false;
}
function exploreWithErrorHandling(id, v, thisValue, args) {
function exploreWithErrorHandling(id, v, thisValue, args, rngSeed) {
try {
explore(id, v, thisValue, args);
explore(id, v, thisValue, args, rngSeed);
} catch (e) {
let line = tryHasProperty('line', e) ? tryGetProperty('line', e) : tryGetProperty('lineNumber', e);
if (isNumber(line)) {
Expand Down
3 changes: 2 additions & 1 deletion Sources/Fuzzilli/Lifting/JavaScriptLifter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -915,7 +915,8 @@ public class JavaScriptLifter: Lifter {
let ID = op.id
let VALUE = input(0)
let ARGS = inputs.dropFirst().map({ $0.text }).joined(separator: ", ")
w.emit("\(EXPLORE)(\"\(ID)\", \(VALUE), this, [\(ARGS)]);")
let RNGSEED = op.rngSeed
w.emit("\(EXPLORE)(\"\(ID)\", \(VALUE), this, [\(ARGS)], \(RNGSEED));")

case .probe(let op):
let PROBE = JavaScriptProbeLifting.probeFunc
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ struct JavaScriptRuntimeAssistedMutatorLifting {
const NumberIsInteger = Number.isInteger;
const isNaN = Number.isNaN;
const isFinite = Number.isFinite;
const random = Math.random;
const truncate = Math.trunc;
const apply = Reflect.apply;
const construct = Reflect.construct;
Expand Down Expand Up @@ -67,6 +66,32 @@ struct JavaScriptRuntimeAssistedMutatorLifting {
const MIN_SAFE_INTEGER = Number.MIN_SAFE_INTEGER;
const MAX_SAFE_INTEGER = Number.MAX_SAFE_INTEGER;
// Simple, seedable PRNG based on a LCG.
class RNG {
m = 2 ** 32;
a = 1664525;
c = 1013904223;
x;
constructor(seed) {
this.x = seed;
}
randomInt() {
this.x = (this.x * this.a + this.c) % this.m;
return this.x;
}
randomFloat() {
return this.randomInt() / this.m;
}
probability(p) {
return this.randomFloat() < p;
}
reseed(seed) {
this.x = seed;
}
}
// When creating empty arrays to which elements are later added, use a custom array type that has a null prototype. This way, the arrays are not
// affected by changes to the Array.prototype that could interfere with array builtins (e.g. indexed setters or a modified .constructor property).
function EmptyArray() {
Expand Down Expand Up @@ -184,14 +209,23 @@ struct JavaScriptRuntimeAssistedMutatorLifting {
//
// Basic random number generation utility functions.
//
// Initially the rng is seeded randomly, specific mutators can reseed() the rng if they need deterministic behavior.
// See the explore operation in JsOperations.swift for an example.
let rng = new RNG(truncate(Math.random() * 2**32));
function probability(p) {
if (p < 0 || p > 1) throw "Argument to probability must be a number between zero and one";
return random() < p;
return rng.probability(p);
}
function randomIntBetween(start, end) {
if (!isInteger(start) || !isInteger(end)) throw "Arguments to randomIntBetween must be integers";
return truncate(random() * (end - start) + start);
return (rng.randomInt() % (end - start)) + start;
}
function randomFloat() {
return rng.randomFloat();
}
function randomBigintBetween(start, end) {
Expand All @@ -202,7 +236,7 @@ struct JavaScriptRuntimeAssistedMutatorLifting {
function randomIntBelow(n) {
if (!isInteger(n)) throw "Argument to randomIntBelow must be an integer";
return truncate(random() * n);
return rng.randomInt() % n;
}
function randomElement(array) {
Expand Down Expand Up @@ -312,7 +346,7 @@ struct JavaScriptRuntimeAssistedMutatorLifting {
// Now choose a random property. If a property has weight 2W, it will be selected with twice the probability of a property with weight W.
let selectedProperty;
let remainingWeight = random() * properties.totalWeight;
let remainingWeight = randomFloat() * properties.totalWeight;
for (let i = 0; i < properties.length; i++) {
let candidate = properties[i];
remainingWeight -= candidate.weight;
Expand Down
16 changes: 12 additions & 4 deletions Sources/Fuzzilli/Protobuf/operations.pb.swift
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ public enum Fuzzilli_Protobuf_PropertyType: SwiftProtobuf.Enum {

extension Fuzzilli_Protobuf_PropertyType: CaseIterable {
// The compiler won't synthesize support with the UNRECOGNIZED case.
public static var allCases: [Fuzzilli_Protobuf_PropertyType] = [
public static let allCases: [Fuzzilli_Protobuf_PropertyType] = [
.value,
.getter,
.setter,
Expand Down Expand Up @@ -132,7 +132,7 @@ public enum Fuzzilli_Protobuf_UnaryOperator: SwiftProtobuf.Enum {

extension Fuzzilli_Protobuf_UnaryOperator: CaseIterable {
// The compiler won't synthesize support with the UNRECOGNIZED case.
public static var allCases: [Fuzzilli_Protobuf_UnaryOperator] = [
public static let allCases: [Fuzzilli_Protobuf_UnaryOperator] = [
.preInc,
.preDec,
.postInc,
Expand Down Expand Up @@ -214,7 +214,7 @@ public enum Fuzzilli_Protobuf_BinaryOperator: SwiftProtobuf.Enum {

extension Fuzzilli_Protobuf_BinaryOperator: CaseIterable {
// The compiler won't synthesize support with the UNRECOGNIZED case.
public static var allCases: [Fuzzilli_Protobuf_BinaryOperator] = [
public static let allCases: [Fuzzilli_Protobuf_BinaryOperator] = [
.add,
.sub,
.mul,
Expand Down Expand Up @@ -284,7 +284,7 @@ public enum Fuzzilli_Protobuf_Comparator: SwiftProtobuf.Enum {

extension Fuzzilli_Protobuf_Comparator: CaseIterable {
// The compiler won't synthesize support with the UNRECOGNIZED case.
public static var allCases: [Fuzzilli_Protobuf_Comparator] = [
public static let allCases: [Fuzzilli_Protobuf_Comparator] = [
.equal,
.strictEqual,
.notEqual,
Expand Down Expand Up @@ -2012,6 +2012,8 @@ public struct Fuzzilli_Protobuf_Explore {

public var id: String = String()

public var rngSeed: Int64 = 0

public var unknownFields = SwiftProtobuf.UnknownStorage()

public init() {}
Expand Down Expand Up @@ -6692,6 +6694,7 @@ extension Fuzzilli_Protobuf_Explore: SwiftProtobuf.Message, SwiftProtobuf._Messa
public static let protoMessageName: String = _protobuf_package + ".Explore"
public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
1: .same(proto: "id"),
2: .same(proto: "rngSeed"),
]

public mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
Expand All @@ -6701,6 +6704,7 @@ extension Fuzzilli_Protobuf_Explore: SwiftProtobuf.Message, SwiftProtobuf._Messa
// enabled. https://github.com/apple/swift-protobuf/issues/1034
switch fieldNumber {
case 1: try { try decoder.decodeSingularStringField(value: &self.id) }()
case 2: try { try decoder.decodeSingularInt64Field(value: &self.rngSeed) }()
default: break
}
}
Expand All @@ -6710,11 +6714,15 @@ extension Fuzzilli_Protobuf_Explore: SwiftProtobuf.Message, SwiftProtobuf._Messa
if !self.id.isEmpty {
try visitor.visitSingularStringField(value: self.id, fieldNumber: 1)
}
if self.rngSeed != 0 {
try visitor.visitSingularInt64Field(value: self.rngSeed, fieldNumber: 2)
}
try unknownFields.traverse(visitor: &visitor)
}

public static func ==(lhs: Fuzzilli_Protobuf_Explore, rhs: Fuzzilli_Protobuf_Explore) -> Bool {
if lhs.id != rhs.id {return false}
if lhs.rngSeed != rhs.rngSeed {return false}
if lhs.unknownFields != rhs.unknownFields {return false}
return true
}
Expand Down
1 change: 1 addition & 0 deletions Sources/Fuzzilli/Protobuf/operations.proto
Original file line number Diff line number Diff line change
Expand Up @@ -592,6 +592,7 @@ message LoadNewTarget {

message Explore {
string id = 1;
int64 rngSeed = 2;
}

message Probe {
Expand Down
2 changes: 2 additions & 0 deletions Sources/Fuzzilli/Protobuf/program.pb.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
// See the License for the specific language governing permissions and
// limitations under the License.

// This file is autogenerated. Please check the README.

import Foundation
import SwiftProtobuf

Expand Down

0 comments on commit 7ccd3e8

Please sign in to comment.