From 2a4fdd4fc4139c77885b97841402024dd3611f48 Mon Sep 17 00:00:00 2001 From: Tobias Wienand Date: Wed, 7 Aug 2024 16:31:24 +0200 Subject: [PATCH 1/9] Implements JS->FuzzIL translatability for switch statements --- Sources/Fuzzilli/Compiler/Compiler.swift | 40 +++- Sources/Fuzzilli/Compiler/Parser/parser.js | 12 ++ Sources/Fuzzilli/Protobuf/ast.pb.swift | 202 +++++++++++++++++++++ Sources/Fuzzilli/Protobuf/ast.proto | 11 ++ 4 files changed, 264 insertions(+), 1 deletion(-) diff --git a/Sources/Fuzzilli/Compiler/Compiler.swift b/Sources/Fuzzilli/Compiler/Compiler.swift index 9accca042..7a7a7f21a 100644 --- a/Sources/Fuzzilli/Compiler/Compiler.swift +++ b/Sources/Fuzzilli/Compiler/Compiler.swift @@ -441,7 +441,6 @@ public class JavaScriptCompiler { emit(EndForOfLoop()) case .breakStatement: - // TODO currently we assume this is a LoopBreak, but once we support switch-statements, it could also be a SwitchBreak emit(LoopBreak()) case .continueStatement: @@ -486,6 +485,45 @@ public class JavaScriptCompiler { try compileBody(withStatement.body) } emit(EndWith()) + case .switchStatement(let switchStatement): + // Precompute tests because between BeginSwitch and BeginSwitchCase we can't emit instructions + var precomputedTests = [Variable?]() + for caseStatement in switchStatement.cases { + if caseStatement.hasTest { + let test = try compileExpression(caseStatement.test) + precomputedTests.append(test) + } else { + precomputedTests.append(nil) + } + } + let discriminant = try compileExpression(switchStatement.discriminant) + emit(BeginSwitch(), withInputs: [discriminant]) + for (index, caseStatement) in switchStatement.cases.enumerated() { + var fallsThrough = true + if caseStatement.hasTest { + if let test = precomputedTests[index] { + emit(BeginSwitchCase(), withInputs: [test]) + } + } else { + emit(BeginSwitchDefaultCase()) + } + try enterNewScope { + for statement in caseStatement.consequent { + switch statement.statement { + case .breakStatement: + fallsThrough = false + break // Don't compile the break statement because it'd be interpreted as a LoopBreak + default: + try compileStatement(statement) + } + if !fallsThrough { + break + } + } + } + emit(EndSwitchCase(fallsThrough: fallsThrough)) + } + emit(EndSwitch()) } } diff --git a/Sources/Fuzzilli/Compiler/Parser/parser.js b/Sources/Fuzzilli/Compiler/Parser/parser.js index 2029c6a57..a129b7e02 100644 --- a/Sources/Fuzzilli/Compiler/Parser/parser.js +++ b/Sources/Fuzzilli/Compiler/Parser/parser.js @@ -326,6 +326,18 @@ function parse(script, proto) { withStatement.body = visitStatement(node.body); return makeStatement('WithStatement', withStatement); } + case 'SwitchStatement': { + let switchStatement = {}; + switchStatement.discriminant = visitExpression(node.discriminant); + switchStatement.cases = node.cases.map(caseNode => visitStatement(caseNode)); + return makeStatement('SwitchStatement', switchStatement); + } + case 'SwitchCase': { + let switchCase = {}; + if (node.test) {switchCase.test = visitExpression(node.test)} + switchCase.consequent = node.consequent.map(consequentNode => visitStatement(consequentNode)); + return switchCase; + } default: { throw "Unhandled node type " + node.type; } diff --git a/Sources/Fuzzilli/Protobuf/ast.pb.swift b/Sources/Fuzzilli/Protobuf/ast.pb.swift index c402adfd5..60d2eb9c5 100644 --- a/Sources/Fuzzilli/Protobuf/ast.pb.swift +++ b/Sources/Fuzzilli/Protobuf/ast.pb.swift @@ -905,6 +905,55 @@ public struct Compiler_Protobuf_WithStatement: @unchecked Sendable { fileprivate var _storage = _StorageClass.defaultInstance } +public struct Compiler_Protobuf_SwitchStatement: @unchecked Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + public var discriminant: Compiler_Protobuf_Expression { + get {return _storage._discriminant ?? Compiler_Protobuf_Expression()} + set {_uniqueStorage()._discriminant = newValue} + } + /// Returns true if `discriminant` has been explicitly set. + public var hasDiscriminant: Bool {return _storage._discriminant != nil} + /// Clears the value of `discriminant`. Subsequent reads from it will return its default value. + public mutating func clearDiscriminant() {_uniqueStorage()._discriminant = nil} + + public var cases: [Compiler_Protobuf_SwitchCase] { + get {return _storage._cases} + set {_uniqueStorage()._cases = newValue} + } + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public init() {} + + fileprivate var _storage = _StorageClass.defaultInstance +} + +public struct Compiler_Protobuf_SwitchCase: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + public var test: Compiler_Protobuf_Expression { + get {return _test ?? Compiler_Protobuf_Expression()} + set {_test = newValue} + } + /// Returns true if `test` has been explicitly set. + public var hasTest: Bool {return self._test != nil} + /// Clears the value of `test`. Subsequent reads from it will return its default value. + public mutating func clearTest() {self._test = nil} + + public var consequent: [Compiler_Protobuf_Statement] = [] + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public init() {} + + fileprivate var _test: Compiler_Protobuf_Expression? = nil +} + public struct Compiler_Protobuf_Statement: @unchecked Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for @@ -1059,6 +1108,14 @@ public struct Compiler_Protobuf_Statement: @unchecked Sendable { set {_uniqueStorage()._statement = .withStatement(newValue)} } + public var switchStatement: Compiler_Protobuf_SwitchStatement { + get { + if case .switchStatement(let v)? = _storage._statement {return v} + return Compiler_Protobuf_SwitchStatement() + } + set {_uniqueStorage()._statement = .switchStatement(newValue)} + } + public var unknownFields = SwiftProtobuf.UnknownStorage() public enum OneOf_Statement: Equatable, Sendable { @@ -1080,6 +1137,7 @@ public struct Compiler_Protobuf_Statement: @unchecked Sendable { case tryStatement(Compiler_Protobuf_TryStatement) case throwStatement(Compiler_Protobuf_ThrowStatement) case withStatement(Compiler_Protobuf_WithStatement) + case switchStatement(Compiler_Protobuf_SwitchStatement) } @@ -4019,6 +4077,132 @@ extension Compiler_Protobuf_WithStatement: SwiftProtobuf.Message, SwiftProtobuf. } } +extension Compiler_Protobuf_SwitchStatement: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = _protobuf_package + ".SwitchStatement" + public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "discriminant"), + 2: .same(proto: "cases"), + ] + + fileprivate class _StorageClass { + var _discriminant: Compiler_Protobuf_Expression? = nil + var _cases: [Compiler_Protobuf_SwitchCase] = [] + + #if swift(>=5.10) + // This property is used as the initial default value for new instances of the type. + // The type itself is protecting the reference to its storage via CoW semantics. + // This will force a copy to be made of this reference when the first mutation occurs; + // hence, it is safe to mark this as `nonisolated(unsafe)`. + static nonisolated(unsafe) let defaultInstance = _StorageClass() + #else + static let defaultInstance = _StorageClass() + #endif + + private init() {} + + init(copying source: _StorageClass) { + _discriminant = source._discriminant + _cases = source._cases + } + } + + fileprivate mutating func _uniqueStorage() -> _StorageClass { + if !isKnownUniquelyReferenced(&_storage) { + _storage = _StorageClass(copying: _storage) + } + return _storage + } + + public mutating func decodeMessage(decoder: inout D) throws { + _ = _uniqueStorage() + try withExtendedLifetime(_storage) { (_storage: _StorageClass) in + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularMessageField(value: &_storage._discriminant) }() + case 2: try { try decoder.decodeRepeatedMessageField(value: &_storage._cases) }() + default: break + } + } + } + } + + public func traverse(visitor: inout V) throws { + try withExtendedLifetime(_storage) { (_storage: _StorageClass) in + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + try { if let v = _storage._discriminant { + try visitor.visitSingularMessageField(value: v, fieldNumber: 1) + } }() + if !_storage._cases.isEmpty { + try visitor.visitRepeatedMessageField(value: _storage._cases, fieldNumber: 2) + } + } + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: Compiler_Protobuf_SwitchStatement, rhs: Compiler_Protobuf_SwitchStatement) -> Bool { + if lhs._storage !== rhs._storage { + let storagesAreEqual: Bool = withExtendedLifetime((lhs._storage, rhs._storage)) { (_args: (_StorageClass, _StorageClass)) in + let _storage = _args.0 + let rhs_storage = _args.1 + if _storage._discriminant != rhs_storage._discriminant {return false} + if _storage._cases != rhs_storage._cases {return false} + return true + } + if !storagesAreEqual {return false} + } + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Compiler_Protobuf_SwitchCase: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = _protobuf_package + ".SwitchCase" + public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "test"), + 2: .same(proto: "consequent"), + ] + + public mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularMessageField(value: &self._test) }() + case 2: try { try decoder.decodeRepeatedMessageField(value: &self.consequent) }() + default: break + } + } + } + + public func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + try { if let v = self._test { + try visitor.visitSingularMessageField(value: v, fieldNumber: 1) + } }() + if !self.consequent.isEmpty { + try visitor.visitRepeatedMessageField(value: self.consequent, fieldNumber: 2) + } + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: Compiler_Protobuf_SwitchCase, rhs: Compiler_Protobuf_SwitchCase) -> Bool { + if lhs._test != rhs._test {return false} + if lhs.consequent != rhs.consequent {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + extension Compiler_Protobuf_Statement: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".Statement" public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ @@ -4040,6 +4224,7 @@ extension Compiler_Protobuf_Statement: SwiftProtobuf.Message, SwiftProtobuf._Mes 16: .same(proto: "tryStatement"), 17: .same(proto: "throwStatement"), 18: .same(proto: "withStatement"), + 19: .same(proto: "switchStatement"), ] fileprivate class _StorageClass { @@ -4311,6 +4496,19 @@ extension Compiler_Protobuf_Statement: SwiftProtobuf.Message, SwiftProtobuf._Mes _storage._statement = .withStatement(v) } }() + case 19: try { + var v: Compiler_Protobuf_SwitchStatement? + var hadOneofValue = false + if let current = _storage._statement { + hadOneofValue = true + if case .switchStatement(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + _storage._statement = .switchStatement(v) + } + }() default: break } } @@ -4396,6 +4594,10 @@ extension Compiler_Protobuf_Statement: SwiftProtobuf.Message, SwiftProtobuf._Mes guard case .withStatement(let v)? = _storage._statement else { preconditionFailure() } try visitor.visitSingularMessageField(value: v, fieldNumber: 18) }() + case .switchStatement?: try { + guard case .switchStatement(let v)? = _storage._statement else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 19) + }() case nil: break } } diff --git a/Sources/Fuzzilli/Protobuf/ast.proto b/Sources/Fuzzilli/Protobuf/ast.proto index 3754b020e..3db9a50c4 100644 --- a/Sources/Fuzzilli/Protobuf/ast.proto +++ b/Sources/Fuzzilli/Protobuf/ast.proto @@ -207,6 +207,16 @@ message WithStatement { Statement body = 2; } +message SwitchStatement { + Expression discriminant = 1; + repeated SwitchCase cases = 2; +} + +message SwitchCase { + Expression test = 1; + repeated Statement consequent = 2; +} + message Statement { oneof statement { EmptyStatement emptyStatement = 1; @@ -227,6 +237,7 @@ message Statement { TryStatement tryStatement = 16; ThrowStatement throwStatement = 17; WithStatement withStatement = 18; + SwitchStatement switchStatement = 19; } } From 1e6b07afa90b6914a99ffdb6f66a807f01c7f8ae Mon Sep 17 00:00:00 2001 From: Tobias Wienand Date: Wed, 7 Aug 2024 16:31:58 +0200 Subject: [PATCH 2/9] Implements compiler tests for the switch statement --- .../CompilerTests/switch_statements.js | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 Tests/FuzzilliTests/CompilerTests/switch_statements.js diff --git a/Tests/FuzzilliTests/CompilerTests/switch_statements.js b/Tests/FuzzilliTests/CompilerTests/switch_statements.js new file mode 100644 index 000000000..929236ced --- /dev/null +++ b/Tests/FuzzilliTests/CompilerTests/switch_statements.js @@ -0,0 +1,26 @@ +let fruit = 'apple'; +for (let i = 0; i < 3; i++) { + switch (fruit) { + case 'apple': // test if this case falls through + console.log('You selected an apple.'); + for (let j = 0; j < 2; j++) { + console.log('Inside apple loop', j); + if (j === 1) { + break; // test if this break exits the inner loop + } + } + case null: + console.log('You selected null.'); + break; // Babel parses default case as null case. Try to confuse the compiler. + default: // test if default case is detected (irrespective of the convention that the last case is the default case) + console.log('Unknown fruit selection.'); // test falls through + break; // test if this break exits the switch + case 'banana': + console.log('You selected a banana.'); + break; // test if this break exits the switch + + } + if (i === 2) { + break; // test if this break exits the outer loop + } +} \ No newline at end of file From 582f1ce82c10fde8dcae055c4c47bea64b2e07da Mon Sep 17 00:00:00 2001 From: Tobias Wienand Date: Mon, 12 Aug 2024 07:59:01 +0200 Subject: [PATCH 3/9] Improves Compiler and JS Parser 1. Shortens the map statement in JS parser 2. precomputedTests in the Compiler now only contains Variables, not nil 3. A TODO comment that proposes dynamic compilation of switch cases is added --- Sources/Fuzzilli/Compiler/Compiler.swift | 16 +++++++--------- Sources/Fuzzilli/Compiler/Parser/parser.js | 2 +- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/Sources/Fuzzilli/Compiler/Compiler.swift b/Sources/Fuzzilli/Compiler/Compiler.swift index 7a7a7f21a..50f3826d7 100644 --- a/Sources/Fuzzilli/Compiler/Compiler.swift +++ b/Sources/Fuzzilli/Compiler/Compiler.swift @@ -486,24 +486,22 @@ public class JavaScriptCompiler { } emit(EndWith()) case .switchStatement(let switchStatement): - // Precompute tests because between BeginSwitch and BeginSwitchCase we can't emit instructions - var precomputedTests = [Variable?]() + // TODO Replace the precomputation of tests with compilation of the test expressions in the cases. + // To do this, we would need to redesign Switch statements in FuzzIL to (for example) have a BeginSwitchCaseHead, BeginSwitchCaseBody, and EndSwitchCase. + // Then the expression would go inside the header. + var precomputedTests = [Variable]() for caseStatement in switchStatement.cases { if caseStatement.hasTest { let test = try compileExpression(caseStatement.test) precomputedTests.append(test) - } else { - precomputedTests.append(nil) - } + } } let discriminant = try compileExpression(switchStatement.discriminant) emit(BeginSwitch(), withInputs: [discriminant]) - for (index, caseStatement) in switchStatement.cases.enumerated() { + for caseStatement in switchStatement.cases { var fallsThrough = true if caseStatement.hasTest { - if let test = precomputedTests[index] { - emit(BeginSwitchCase(), withInputs: [test]) - } + emit(BeginSwitchCase(), withInputs: [precomputedTests.removeFirst()]) } else { emit(BeginSwitchDefaultCase()) } diff --git a/Sources/Fuzzilli/Compiler/Parser/parser.js b/Sources/Fuzzilli/Compiler/Parser/parser.js index a129b7e02..653161749 100644 --- a/Sources/Fuzzilli/Compiler/Parser/parser.js +++ b/Sources/Fuzzilli/Compiler/Parser/parser.js @@ -329,7 +329,7 @@ function parse(script, proto) { case 'SwitchStatement': { let switchStatement = {}; switchStatement.discriminant = visitExpression(node.discriminant); - switchStatement.cases = node.cases.map(caseNode => visitStatement(caseNode)); + switchStatement.cases = node.cases.map(visitStatement); return makeStatement('SwitchStatement', switchStatement); } case 'SwitchCase': { From 1f85a8846d567f8bc75ef23bfffacbda546f8ddb Mon Sep 17 00:00:00 2001 From: Tobias Wienand Date: Tue, 13 Aug 2024 10:55:05 +0200 Subject: [PATCH 4/9] Implement robust break context identification This change ensures that when a break statement is encountered in the Compiler, the appropriate break type (LoopBreak or SwitchBreak) is emitted based on context. 3 out 4 cases are not a problem: - Case 1: If neither .loop nor .switchBlock is in context, an error is raised. - Case 2: If .loop is in context but not .switchBlock, emit LoopBreak. - Case 3: If .switchBlock is in context but not .loop, emit SwitchBreak. - Case 4 (problem): If both are in context, the innermost context needs to be determined. For Case 4, the existing contextStack doesn't track which context was opened most recently. A new breakContextStack is introduced to handle this. Minor modifications to the Instruction and Attributes classes were made to manage pushing and popping of break context information. --- Sources/Fuzzilli/Compiler/Compiler.swift | 29 ++++++++++++---------- Sources/Fuzzilli/FuzzIL/Analyzer.swift | 14 +++++++++++ Sources/Fuzzilli/FuzzIL/Instruction.swift | 5 ++++ Sources/Fuzzilli/FuzzIL/JsOperations.swift | 14 +++++------ Sources/Fuzzilli/FuzzIL/Operation.swift | 3 +++ 5 files changed, 45 insertions(+), 20 deletions(-) diff --git a/Sources/Fuzzilli/Compiler/Compiler.swift b/Sources/Fuzzilli/Compiler/Compiler.swift index 50f3826d7..f2b40a038 100644 --- a/Sources/Fuzzilli/Compiler/Compiler.swift +++ b/Sources/Fuzzilli/Compiler/Compiler.swift @@ -57,6 +57,9 @@ public class JavaScriptCompiler { /// The next free FuzzIL variable. private var nextVariable = 0 + /// Context analyzer to track the context of the code being compiled. Used to distinguish switch and loop breaks. + private var contextAnalyzer = ContextAnalyzer() + public func compile(_ ast: AST) throws -> Program { reset() @@ -441,7 +444,16 @@ public class JavaScriptCompiler { emit(EndForOfLoop()) case .breakStatement: - emit(LoopBreak()) + switch contextAnalyzer.breakContext { + case .loop: + emit(LoopBreak()) + break + case .switchBlock: + emit(SwitchBreak()) + break + default: + throw CompilerError.invalidNodeError("break statement outside of loop or switch") + } case .continueStatement: emit(LoopContinue()) @@ -499,7 +511,6 @@ public class JavaScriptCompiler { let discriminant = try compileExpression(switchStatement.discriminant) emit(BeginSwitch(), withInputs: [discriminant]) for caseStatement in switchStatement.cases { - var fallsThrough = true if caseStatement.hasTest { emit(BeginSwitchCase(), withInputs: [precomputedTests.removeFirst()]) } else { @@ -507,19 +518,10 @@ public class JavaScriptCompiler { } try enterNewScope { for statement in caseStatement.consequent { - switch statement.statement { - case .breakStatement: - fallsThrough = false - break // Don't compile the break statement because it'd be interpreted as a LoopBreak - default: - try compileStatement(statement) - } - if !fallsThrough { - break - } + try compileStatement(statement) } } - emit(EndSwitchCase(fallsThrough: fallsThrough)) + emit(EndSwitchCase(fallsThrough: true)) } emit(EndSwitch()) } @@ -1035,6 +1037,7 @@ public class JavaScriptCompiler { let innerOutputs = (0..(_ op: Operation, inouts: Variables, index: Int? = nil) where Variables.Element == Variable { assert(op.numInputs + op.numOutputs + op.numInnerOutputs == inouts.count) diff --git a/Sources/Fuzzilli/FuzzIL/JsOperations.swift b/Sources/Fuzzilli/FuzzIL/JsOperations.swift index bab2bd749..f15c7cbd2 100644 --- a/Sources/Fuzzilli/FuzzIL/JsOperations.swift +++ b/Sources/Fuzzilli/FuzzIL/JsOperations.swift @@ -1851,7 +1851,7 @@ final class EndWhileLoop: JsOperation { override var opcode: Opcode { .endWhileLoop(self) } init() { - super.init(attributes: .isBlockEnd) + super.init(attributes: [.isBlockEnd, .isBreakableEnd]) } } @@ -1876,7 +1876,7 @@ final class EndDoWhileLoop: JsOperation { override var opcode: Opcode { .endDoWhileLoop(self) } init() { - super.init(numInputs: 1, attributes: .isBlockEnd) + super.init(numInputs: 1, attributes: [.isBlockEnd, .isBreakableEnd]) } } @@ -1960,7 +1960,7 @@ final class EndForLoop: JsOperation { override var opcode: Opcode { .endForLoop(self) } init() { - super.init(attributes: .isBlockEnd) + super.init(attributes: [.isBlockEnd, .isBreakableEnd]) } } @@ -1976,7 +1976,7 @@ final class EndForInLoop: JsOperation { override var opcode: Opcode { .endForInLoop(self) } init() { - super.init(attributes: .isBlockEnd) + super.init(attributes: [.isBlockEnd, .isBreakableEnd]) } } @@ -2006,7 +2006,7 @@ final class EndForOfLoop: JsOperation { override var opcode: Opcode { .endForOfLoop(self) } init() { - super.init(attributes: .isBlockEnd) + super.init(attributes: [.isBlockEnd, .isBreakableEnd]) } } @@ -2034,7 +2034,7 @@ final class EndRepeatLoop: JsOperation { override var opcode: Opcode { .endRepeatLoop(self) } init() { - super.init(attributes: .isBlockEnd) + super.init(attributes: [.isBlockEnd, .isBreakableEnd]) } } @@ -2222,7 +2222,7 @@ final class EndSwitch: JsOperation { override var opcode: Opcode { .endSwitch(self) } init() { - super.init(attributes: .isBlockEnd, requiredContext: .switchBlock) + super.init(attributes: [.isBlockEnd, .isBreakableEnd], requiredContext: .switchBlock) } } diff --git a/Sources/Fuzzilli/FuzzIL/Operation.swift b/Sources/Fuzzilli/FuzzIL/Operation.swift index f255bb562..8f13747ae 100644 --- a/Sources/Fuzzilli/FuzzIL/Operation.swift +++ b/Sources/Fuzzilli/FuzzIL/Operation.swift @@ -138,6 +138,9 @@ public class Operation { // The instruction is a Nop operation. static let isNop = Attributes(rawValue: 1 << 11) + + // The instruction ends a breakable context. + static let isBreakableEnd = Attributes(rawValue: 1 << 12) } } From 1fb1fe54c6970c63e833eb05c33283f9d03b6f82 Mon Sep 17 00:00:00 2001 From: Tobias Wienand Date: Tue, 13 Aug 2024 11:59:15 +0200 Subject: [PATCH 5/9] refactor: simplify switch case consequent mapping in parser.js --- Sources/Fuzzilli/Compiler/Parser/parser.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Fuzzilli/Compiler/Parser/parser.js b/Sources/Fuzzilli/Compiler/Parser/parser.js index 653161749..07c886919 100644 --- a/Sources/Fuzzilli/Compiler/Parser/parser.js +++ b/Sources/Fuzzilli/Compiler/Parser/parser.js @@ -335,7 +335,7 @@ function parse(script, proto) { case 'SwitchCase': { let switchCase = {}; if (node.test) {switchCase.test = visitExpression(node.test)} - switchCase.consequent = node.consequent.map(consequentNode => visitStatement(consequentNode)); + switchCase.consequent = node.consequent.map(visitStatement); return switchCase; } default: { From e7a3e2aa9058d648b0318f6d2675eae66e0b4b8d Mon Sep 17 00:00:00 2001 From: Tobias Wienand Date: Tue, 13 Aug 2024 16:47:25 +0200 Subject: [PATCH 6/9] Improves/adds two comments in Compiler.swift --- Sources/Fuzzilli/Compiler/Compiler.swift | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Sources/Fuzzilli/Compiler/Compiler.swift b/Sources/Fuzzilli/Compiler/Compiler.swift index f2b40a038..5ae91c0ac 100644 --- a/Sources/Fuzzilli/Compiler/Compiler.swift +++ b/Sources/Fuzzilli/Compiler/Compiler.swift @@ -57,7 +57,7 @@ public class JavaScriptCompiler { /// The next free FuzzIL variable. private var nextVariable = 0 - /// Context analyzer to track the context of the code being compiled. Used to distinguish switch and loop breaks. + /// Context analyzer to track the context of the code being compiled. Used for example to distinguish switch and loop breaks. private var contextAnalyzer = ContextAnalyzer() public func compile(_ ast: AST) throws -> Program { @@ -521,7 +521,9 @@ public class JavaScriptCompiler { try compileStatement(statement) } } - emit(EndSwitchCase(fallsThrough: true)) + // We could also do an optimization here where we check if the last statement in the case is a break, and if so, we drop the last instruction + // and set the fallsThrough flag to false. + emit(EndSwitchCase(fallsThrough: true)) } emit(EndSwitch()) } From 5a4177e8bbfdc76e6a0e7c1628ca160202b5a82a Mon Sep 17 00:00:00 2001 From: Tobias Wienand Date: Mon, 19 Aug 2024 20:52:58 -0500 Subject: [PATCH 7/9] Makes break context identification lightweight These changes move the logic for break context identification out of the performance ciritical part of the Fuzzilli project and into the compiler. The idea is straight-forward: We fix the bug in the context analyzer that caused contexts to contain both .loop and .switchBlock by simply removing the loop context upon opening a switchBlock context and vice versa. --- Sources/Fuzzilli/Compiler/Compiler.swift | 7 +++---- Sources/Fuzzilli/FuzzIL/Analyzer.swift | 19 +++++-------------- Sources/Fuzzilli/FuzzIL/Instruction.swift | 5 ----- Sources/Fuzzilli/FuzzIL/JsOperations.swift | 14 +++++++------- Sources/Fuzzilli/FuzzIL/Operation.swift | 3 --- 5 files changed, 15 insertions(+), 33 deletions(-) diff --git a/Sources/Fuzzilli/Compiler/Compiler.swift b/Sources/Fuzzilli/Compiler/Compiler.swift index 5ae91c0ac..114d96b1b 100644 --- a/Sources/Fuzzilli/Compiler/Compiler.swift +++ b/Sources/Fuzzilli/Compiler/Compiler.swift @@ -444,14 +444,13 @@ public class JavaScriptCompiler { emit(EndForOfLoop()) case .breakStatement: - switch contextAnalyzer.breakContext { - case .loop: + if contextAnalyzer.context.contains(.loop){ emit(LoopBreak()) break - case .switchBlock: + } else if contextAnalyzer.context.contains(.switchBlock){ emit(SwitchBreak()) break - default: + } else { throw CompilerError.invalidNodeError("break statement outside of loop or switch") } diff --git a/Sources/Fuzzilli/FuzzIL/Analyzer.swift b/Sources/Fuzzilli/FuzzIL/Analyzer.swift index 85d258b49..2d739a31a 100644 --- a/Sources/Fuzzilli/FuzzIL/Analyzer.swift +++ b/Sources/Fuzzilli/FuzzIL/Analyzer.swift @@ -142,16 +142,11 @@ struct VariableAnalyzer: Analyzer { /// Keeps track of the current context during program construction. struct ContextAnalyzer: Analyzer { private var contextStack = Stack([Context.javascript]) - private var breakContextStack = Stack([Context.empty]) var context: Context { return contextStack.top } - var breakContext: Context { - return breakContextStack.top - } - mutating func analyze(_ instr: Instruction) { if instr.isBlockEnd { contextStack.pop() @@ -173,17 +168,13 @@ struct ContextAnalyzer: Analyzer { newContext.formUnion(contextStack.secondToTop) } + if (instr.op.contextOpened.contains(.switchBlock)) { + newContext.remove(.loop) + } else if (instr.op.contextOpened.contains(.loop)) { + newContext.remove(.switchBlock) + } contextStack.push(newContext) } - if instr.op.contextOpened.contains(.loop){ - breakContextStack.push(.loop) - } - if instr.op.contextOpened.contains(.switchBlock) { - breakContextStack.push(.switchBlock) - } - if instr.isBreakableEnd { - breakContextStack.pop() - } } } diff --git a/Sources/Fuzzilli/FuzzIL/Instruction.swift b/Sources/Fuzzilli/FuzzIL/Instruction.swift index 3ea4a0237..80eac2c04 100644 --- a/Sources/Fuzzilli/FuzzIL/Instruction.swift +++ b/Sources/Fuzzilli/FuzzIL/Instruction.swift @@ -251,11 +251,6 @@ public struct Instruction { return op.attributes.contains(.isNop) } - /// Whether this instruction is the end of a breakable context. - public var isBreakableEnd: Bool { - return op.attributes.contains(.isBreakableEnd) - } - public init(_ op: Operation, inouts: Variables, index: Int? = nil) where Variables.Element == Variable { assert(op.numInputs + op.numOutputs + op.numInnerOutputs == inouts.count) diff --git a/Sources/Fuzzilli/FuzzIL/JsOperations.swift b/Sources/Fuzzilli/FuzzIL/JsOperations.swift index f15c7cbd2..bab2bd749 100644 --- a/Sources/Fuzzilli/FuzzIL/JsOperations.swift +++ b/Sources/Fuzzilli/FuzzIL/JsOperations.swift @@ -1851,7 +1851,7 @@ final class EndWhileLoop: JsOperation { override var opcode: Opcode { .endWhileLoop(self) } init() { - super.init(attributes: [.isBlockEnd, .isBreakableEnd]) + super.init(attributes: .isBlockEnd) } } @@ -1876,7 +1876,7 @@ final class EndDoWhileLoop: JsOperation { override var opcode: Opcode { .endDoWhileLoop(self) } init() { - super.init(numInputs: 1, attributes: [.isBlockEnd, .isBreakableEnd]) + super.init(numInputs: 1, attributes: .isBlockEnd) } } @@ -1960,7 +1960,7 @@ final class EndForLoop: JsOperation { override var opcode: Opcode { .endForLoop(self) } init() { - super.init(attributes: [.isBlockEnd, .isBreakableEnd]) + super.init(attributes: .isBlockEnd) } } @@ -1976,7 +1976,7 @@ final class EndForInLoop: JsOperation { override var opcode: Opcode { .endForInLoop(self) } init() { - super.init(attributes: [.isBlockEnd, .isBreakableEnd]) + super.init(attributes: .isBlockEnd) } } @@ -2006,7 +2006,7 @@ final class EndForOfLoop: JsOperation { override var opcode: Opcode { .endForOfLoop(self) } init() { - super.init(attributes: [.isBlockEnd, .isBreakableEnd]) + super.init(attributes: .isBlockEnd) } } @@ -2034,7 +2034,7 @@ final class EndRepeatLoop: JsOperation { override var opcode: Opcode { .endRepeatLoop(self) } init() { - super.init(attributes: [.isBlockEnd, .isBreakableEnd]) + super.init(attributes: .isBlockEnd) } } @@ -2222,7 +2222,7 @@ final class EndSwitch: JsOperation { override var opcode: Opcode { .endSwitch(self) } init() { - super.init(attributes: [.isBlockEnd, .isBreakableEnd], requiredContext: .switchBlock) + super.init(attributes: .isBlockEnd, requiredContext: .switchBlock) } } diff --git a/Sources/Fuzzilli/FuzzIL/Operation.swift b/Sources/Fuzzilli/FuzzIL/Operation.swift index 8f13747ae..f255bb562 100644 --- a/Sources/Fuzzilli/FuzzIL/Operation.swift +++ b/Sources/Fuzzilli/FuzzIL/Operation.swift @@ -138,9 +138,6 @@ public class Operation { // The instruction is a Nop operation. static let isNop = Attributes(rawValue: 1 << 11) - - // The instruction ends a breakable context. - static let isBreakableEnd = Attributes(rawValue: 1 << 12) } } From 1807044d2f3146662e5c52caa47646b5079611d5 Mon Sep 17 00:00:00 2001 From: Tobias Wienand Date: Thu, 22 Aug 2024 10:47:46 +0200 Subject: [PATCH 8/9] Refactoring: Comments and unnecessary breaks --- Sources/Fuzzilli/Compiler/Compiler.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/Fuzzilli/Compiler/Compiler.swift b/Sources/Fuzzilli/Compiler/Compiler.swift index 114d96b1b..dedd822af 100644 --- a/Sources/Fuzzilli/Compiler/Compiler.swift +++ b/Sources/Fuzzilli/Compiler/Compiler.swift @@ -444,12 +444,12 @@ public class JavaScriptCompiler { emit(EndForOfLoop()) case .breakStatement: + // If we're in both .loop and .switch context, then the loop must be the most recent context + // (switch blocks don't propagate an outer .loop context) so we just need to check for .loop here if contextAnalyzer.context.contains(.loop){ emit(LoopBreak()) - break } else if contextAnalyzer.context.contains(.switchBlock){ emit(SwitchBreak()) - break } else { throw CompilerError.invalidNodeError("break statement outside of loop or switch") } From 481be3c824a5c4f4b85cffdce9f921a53951f773 Mon Sep 17 00:00:00 2001 From: Tobias Wienand Date: Thu, 22 Aug 2024 11:24:55 +0200 Subject: [PATCH 9/9] Revers Analyzer to old state to move any Analyzer changes to a seperate PR --- Sources/Fuzzilli/Compiler/Compiler.swift | 1 + Sources/Fuzzilli/FuzzIL/Analyzer.swift | 5 ----- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/Sources/Fuzzilli/Compiler/Compiler.swift b/Sources/Fuzzilli/Compiler/Compiler.swift index dedd822af..d12020334 100644 --- a/Sources/Fuzzilli/Compiler/Compiler.swift +++ b/Sources/Fuzzilli/Compiler/Compiler.swift @@ -446,6 +446,7 @@ public class JavaScriptCompiler { case .breakStatement: // If we're in both .loop and .switch context, then the loop must be the most recent context // (switch blocks don't propagate an outer .loop context) so we just need to check for .loop here + // TODO remove this comment once the Analyzer bug fixs has been merged. Until then the code in this switch case is buggy. if contextAnalyzer.context.contains(.loop){ emit(LoopBreak()) } else if contextAnalyzer.context.contains(.switchBlock){ diff --git a/Sources/Fuzzilli/FuzzIL/Analyzer.swift b/Sources/Fuzzilli/FuzzIL/Analyzer.swift index 2d739a31a..c43f99045 100644 --- a/Sources/Fuzzilli/FuzzIL/Analyzer.swift +++ b/Sources/Fuzzilli/FuzzIL/Analyzer.swift @@ -168,11 +168,6 @@ struct ContextAnalyzer: Analyzer { newContext.formUnion(contextStack.secondToTop) } - if (instr.op.contextOpened.contains(.switchBlock)) { - newContext.remove(.loop) - } else if (instr.op.contextOpened.contains(.loop)) { - newContext.remove(.switchBlock) - } contextStack.push(newContext) } }