diff --git a/Sources/Fuzzilli/Environment/JavaScriptEnvironment.swift b/Sources/Fuzzilli/Environment/JavaScriptEnvironment.swift index 8516321e..19cc4f83 100644 --- a/Sources/Fuzzilli/Environment/JavaScriptEnvironment.swift +++ b/Sources/Fuzzilli/Environment/JavaScriptEnvironment.swift @@ -48,8 +48,65 @@ public class JavaScriptEnvironment: ComponentBase, Environment { // TODO more? public let interestingStrings = jsTypeNames - // TODO more? - public let interestingRegExps = [".", "\\d", "\\w", "\\s", "\\D", "\\W", "\\S"] + public let interestingRegExps = [ + ".", // Always matches. + "\\P{Any}", // Never matches. + "^", // Zero-width assertion, matches once. + "(?=.)", // Zero-width assertion, matches at every position. + "\\b", // Zero-width assertion, matches at each word boundary. + "()", // Zero-width assertion, matches at every position with groups. + "(?)", // Likewise but with named groups. + "((((.).).).)", "(?(?(?(?.).).).)", + // Copied from + // https://cs.chromium.org/chromium/src/testing/libfuzzer/fuzzers/dicts/regexp.dict + "?", "abc", "()", "[]", "abc|def", "abc|def|ghi", "^xxx$", + "ab\\b\\d\\bcd", "\\w|\\d", "a*?", "abc+", "abc+?", "xyz?", "xyz??", + "xyz{0,1}", "xyz{0,1}?", "xyz{93}", "xyz{1,32}", "xyz{1,32}?", "xyz{1,}", + "xyz{1,}?", "a\\fb\\nc\\rd\\te\\vf", "a\\nb\\bc", "(?:foo)", "(?: foo )", + "foo|(bar|baz)|quux", "foo(?=bar)baz", "foo(?!bar)baz", "foo(?<=bar)baz", + "foo(?)", "(?.)", + "(?.)\\k", "\\p{Script=Greek}", "\\P{sc=Greek}", + "\\p{Script_Extensions=Greek}", "\\P{scx=Greek}", + "\\p{General_Category=Decimal_Number}", "\\P{gc=Decimal_Number}", + "\\p{gc=Nd}", "\\P{Decimal_Number}", "\\p{Nd}", "\\P{Any}", + "\\p{Changes_When_NFKC_Casefolded}", + "[\\p{Script_Extensions=Greek}--[α-γ]]", + "[\\p{Script_Extensions=Mongolian}&&\\p{Number}]", + "[\\q{abc|def|0|5}--\\d]" + ] + public let interestingRegExpQuantifiers = ["*", "+", "?"] public let intType = JSType.integer diff --git a/Sources/Fuzzilli/FuzzIL/JsOperations.swift b/Sources/Fuzzilli/FuzzIL/JsOperations.swift index 50cdbf4e..2d8d0c27 100644 --- a/Sources/Fuzzilli/FuzzIL/JsOperations.swift +++ b/Sources/Fuzzilli/FuzzIL/JsOperations.swift @@ -154,6 +154,10 @@ public struct RegExpFlags: OptionSet, Hashable { public func asString() -> String { var strRepr = "" + + // These flags are mutually exclusive, will lead to runtime exceptions if used together + assert(!(contains(.unicode) && contains(.unicodeSets))) + for (flag, char) in RegExpFlags.flagToCharDict { if contains(flag) { strRepr += char @@ -178,22 +182,39 @@ public struct RegExpFlags: OptionSet, Hashable { flags.formUnion(.unicode) case "y": flags.formUnion(.sticky) + case "d": + flags.formUnion(.hasIndices) + case "v": + flags.formUnion(.unicodeSets) default: return nil } } + // These flags are mutually exclusive, will lead to runtime exceptions if used together + assert(!(flags.contains(.unicode) && flags.contains(.unicodeSets))) return flags } - static let caseInsensitive = RegExpFlags(rawValue: 1 << 0) - static let global = RegExpFlags(rawValue: 1 << 1) - static let multiline = RegExpFlags(rawValue: 1 << 2) - static let dotall = RegExpFlags(rawValue: 1 << 3) - static let unicode = RegExpFlags(rawValue: 1 << 4) - static let sticky = RegExpFlags(rawValue: 1 << 5) + static let caseInsensitive = RegExpFlags(rawValue: 1 << 0) // i + static let global = RegExpFlags(rawValue: 1 << 1) // g + static let multiline = RegExpFlags(rawValue: 1 << 2) // m + static let dotall = RegExpFlags(rawValue: 1 << 3) // s + static let unicode = RegExpFlags(rawValue: 1 << 4) // u + static let sticky = RegExpFlags(rawValue: 1 << 5) // y + static let hasIndices = RegExpFlags(rawValue: 1 << 6) // d + static let unicodeSets = RegExpFlags(rawValue: 1 << 7) // v public static func random() -> RegExpFlags { - return RegExpFlags(rawValue: UInt32.random(in: 0..<(1<<6))) + var flags = RegExpFlags(rawValue: UInt32.random(in: 0..<(1<<8))) + if flags.contains(.unicode) && flags.contains(.unicodeSets) { + // clear one of them as they are mutually exclusive, they will throw a runtime exception if used together. + withEqualProbability({ + flags.subtract(.unicode) + }, { + flags.subtract(.unicodeSets) + }) + } + return flags } private static let flagToCharDict: [RegExpFlags:String] = [ @@ -203,6 +224,8 @@ public struct RegExpFlags: OptionSet, Hashable { .dotall: "s", .unicode: "u", .sticky: "y", + .hasIndices: "d", + .unicodeSets: "v", ] } diff --git a/Sources/FuzzilliCli/Profiles/V8Profile.swift b/Sources/FuzzilliCli/Profiles/V8Profile.swift index ceb91b23..ee2ba0a0 100644 --- a/Sources/FuzzilliCli/Profiles/V8Profile.swift +++ b/Sources/FuzzilliCli/Profiles/V8Profile.swift @@ -304,6 +304,106 @@ fileprivate let MapTransitionsTemplate = ProgramTemplate("MapTransitionsTemplate } } +// This template fuzzes the RegExp engine. +// It finds bugs like: crbug.com/1437346 and crbug.com/1439691. +fileprivate let RegExpFuzzerTemplate = ProgramTemplate("RegExpFuzzerTemplate") { b in + // Taken from: https://source.chromium.org/chromium/chromium/src/+/refs/heads/main:v8/test/fuzzer/regexp-builtins.cc;l=212;drc=a61b95c63b0b75c1cfe872d9c8cdf927c226046e + let twoByteSubjectString = "f\\uD83D\\uDCA9ba\\u2603" + + let replacementCandidates = [ + "'X'", + "'$1$2$3'", + "'$$$&$`$\\'$1'", + "() => 'X'", + "(arg0, arg1, arg2, arg3, arg4) => arg0 + arg1 + arg2 + arg3 + arg4", + "() => 42" + ] + + let lastIndices = [ + "undefined", "-1", "0", + "1", "2", "3", + "4", "5", "6", + "7", "8", "9", + "50", "4294967296", "2147483647", + "2147483648", "NaN", "Not a Number" + ] + + let f = b.buildPlainFunction(with: .parameters(n: 0)) { _ in + let pattern = probability(0.5) ? chooseUniform(from: b.fuzzer.environment.interestingRegExps) : b.randomString() + let regExpVar = b.loadRegExp(pattern, RegExpFlags.random()) + + let lastIndex = chooseUniform(from: lastIndices) + let lastIndexString = b.loadString(lastIndex) + + b.setProperty("lastIndex", of: regExpVar, to: lastIndexString) + + let subjectVar: Variable + + if probability(0.1) { + subjectVar = b.loadString(twoByteSubjectString) + } else { + subjectVar = b.loadString(b.randomString()) + } + + let resultVar = b.loadNull() + + b.buildTryCatchFinally(tryBody: { + let symbol = b.loadBuiltin("Symbol") + withEqualProbability({ + let res = b.callMethod("exec", on: regExpVar, withArgs: [subjectVar]) + b.reassign(resultVar, to: res) + }, { + let prop = b.getProperty("match", of: symbol) + let res = b.callComputedMethod(prop, on: regExpVar, withArgs: [subjectVar]) + b.reassign(resultVar, to: res) + }, { + let prop = b.getProperty("replace", of: symbol) + let replacement = withEqualProbability({ + b.loadString(b.randomString()) + }, { + b.loadString(chooseUniform(from: replacementCandidates)) + }) + let res = b.callComputedMethod(prop, on: regExpVar, withArgs: [subjectVar, replacement]) + b.reassign(resultVar, to: res) + }, { + let prop = b.getProperty("search", of: symbol) + let res = b.callComputedMethod(prop, on: regExpVar, withArgs: [subjectVar]) + b.reassign(resultVar, to: res) + }, { + let prop = b.getProperty("split", of: symbol) + let randomSplitLimit = withEqualProbability({ + "undefined" + }, { + "'not a number'" + }, { + String(b.randomInt()) + }) + let limit = b.loadString(randomSplitLimit) + let res = b.callComputedMethod(symbol, on: regExpVar, withArgs: [subjectVar, limit]) + b.reassign(resultVar, to: res) + }, { + let res = b.callMethod("test", on: regExpVar, withArgs: [subjectVar]) + b.reassign(resultVar, to: res) + }) + }, catchBody: { _ in + }) + + b.build(n: 7) + + b.doReturn(resultVar) + } + + b.eval("%SetForceSlowPath(false)"); + // compile the regexp once + b.callFunction(f) + let resFast = b.callFunction(f) + b.eval("%SetForceSlowPath(true)"); + let resSlow = b.callFunction(f) + b.eval("%SetForceSlowPath(false)"); + + b.build(n: 15) +} + let v8Profile = Profile( processArgs: { randomize in var args = [ @@ -432,6 +532,7 @@ let v8Profile = Profile( additionalProgramTemplates: WeightedList([ (MapTransitionsTemplate, 1), + (RegExpFuzzerTemplate, 1), ]), disabledCodeGenerators: [],