Skip to content

Commit 48163c8

Browse files
committed
Improve RegExp Fuzzing.
Improve RegExp Fuzzing by adding more interesting patterns and using a template based on the regexp-builtins.cc fuzzer by jgruber@.
1 parent 28731f8 commit 48163c8

3 files changed

Lines changed: 190 additions & 9 deletions

File tree

Sources/Fuzzilli/Environment/JavaScriptEnvironment.swift

Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,65 @@ public class JavaScriptEnvironment: ComponentBase, Environment {
4848
// TODO more?
4949
public let interestingStrings = jsTypeNames
5050

51-
// TODO more?
52-
public let interestingRegExps = [".", "\\d", "\\w", "\\s", "\\D", "\\W", "\\S"]
51+
public let interestingRegExps = [
52+
".", // Always matches.
53+
"\\P{Any}", // Never matches.
54+
"^", // Zero-width assertion, matches once.
55+
"(?=.)", // Zero-width assertion, matches at every position.
56+
"\\b", // Zero-width assertion, matches at each word boundary.
57+
"()", // Zero-width assertion, matches at every position with groups.
58+
"(?<a>)", // Likewise but with named groups.
59+
"((((.).).).)", "(?<a>(?<b>(?<c>(?<d>.).).).)",
60+
// Copied from
61+
// https://cs.chromium.org/chromium/src/testing/libfuzzer/fuzzers/dicts/regexp.dict
62+
"?", "abc", "()", "[]", "abc|def", "abc|def|ghi", "^xxx$",
63+
"ab\\b\\d\\bcd", "\\w|\\d", "a*?", "abc+", "abc+?", "xyz?", "xyz??",
64+
"xyz{0,1}", "xyz{0,1}?", "xyz{93}", "xyz{1,32}", "xyz{1,32}?", "xyz{1,}",
65+
"xyz{1,}?", "a\\fb\\nc\\rd\\te\\vf", "a\\nb\\bc", "(?:foo)", "(?: foo )",
66+
"foo|(bar|baz)|quux", "foo(?=bar)baz", "foo(?!bar)baz", "foo(?<=bar)baz",
67+
"foo(?<!bar)baz", "()", "(?=)", "[]", "[x]", "[xyz]", "[a-zA-Z0-9]",
68+
"[-123]", "[^123]", "]", "}", "[a-b-c]", "[x\\dz]", "[\\d-z]",
69+
"[\\d-\\d]", "[z-\\d]", "\\cj\\cJ\\ci\\cI\\ck\\cK", "\\c!", "\\c_",
70+
"\\c~", "[\\c!]", "[\\c_]", "[\\c~]", "[\\ca]", "[\\cz]", "[\\cA]",
71+
"[\\cZ]", "[\\c1]", "\\[\\]\\{\\}\\(\\)\\%\\^\\#\\ ",
72+
"[\\[\\]\\{\\}\\(\\)\\%\\^\\#\\ ]", "\\8", "\\9", "\\11", "\\11a",
73+
"\\011", "\\118", "\\111", "\\1111", "(x)(x)(x)\\1", "(x)(x)(x)\\2",
74+
"(x)(x)(x)\\3", "(x)(x)(x)\\4", "(x)(x)(x)\\1*", "(x)(x)(x)\\3*",
75+
"(x)(x)(x)\\4*", "(x)(x)(x)(x)(x)(x)(x)(x)(x)(x)\\10",
76+
"(x)(x)(x)(x)(x)(x)(x)(x)(x)(x)\\11", "(a)\\1", "(a\\1)", "(\\1a)",
77+
"(\\2)(\\1)", "(?=a){0,10}a", "(?=a){1,10}a", "(?=a){9,10}a", "(?!a)?a",
78+
"\\1(a)", "(?!(a))\\1", "(?!\\1(a\\1)\\1)\\1",
79+
"\\1\\2(a(?:\\1(b\\1\\2))\\2)\\1", "[\\0]", "[\\11]", "[\\11a]",
80+
"[\\011]", "[\\00011]", "[\\118]", "[\\111]", "[\\1111]", "\\x60",
81+
"\\x3z", "\\c", "\\u0034", "\\u003z", "foo[z]*", "\\u{12345}",
82+
"\\u{12345}\\u{23456}", "\\u{12345}{3}", "\\u{12345}*", "\\ud808\\udf45*",
83+
"[\\ud808\\udf45-\\ud809\\udccc]", "a", "a|b", "a\\n", "a$", "a\\b!",
84+
"a\\Bb", "a*?", "a?", "a??", "a{0,1}?", "a{1,2}?", "a+?", "(a)", "(a)\\1",
85+
"(\\1a)", "\\1(a)", "a\\s", "a\\S", "a\\D", "a\\w", "a\\W", "a.", "a\\q",
86+
"a[a]", "a[^a]", "a[a-z]", "a(?:b)", "a(?=b)", "a(?!b)", "\\x60",
87+
"\\u0060", "\\cA", "\\q", "\\1112", "(a)\\1", "(?!a)?a\\1",
88+
"(?:(?=a))a\\1", "a{}", "a{,}", "a{", "a{z}", "a{12z}", "a{12,",
89+
"a{12,3b", "{}", "{,}", "{", "{z}", "{1z}", "{12,", "{12,3b", "a", "abc",
90+
"a[bc]d", "a|bc", "ab|c", "a||bc", "(?:ab)", "(?:ab|cde)", "(?:ab)|cde",
91+
"(ab)", "(ab|cde)", "(ab)\\1", "(ab|cde)\\1", "(?:ab)?", "(?:ab)+", "a?",
92+
"a+", "a??", "a*?", "a+?", "(?:a?)?", "(?:a+)?", "(?:a?)+", "(?:a*)+",
93+
"(?:a+)+", "(?:a?)*", "(?:a*)*", "(?:a+)*", "a{0}", "(?:a+){0,0}", "a*b",
94+
"a+b", "a*b|c", "a+b|c", "(?:a{5,1000000}){3,1000000}", "(?:ab){4,7}",
95+
"a\\bc", "a\\sc", "a\\Sc", "a(?=b)c", "a(?=bbb|bb)c", "a(?!bbb|bb)c",
96+
"\\xe2\\x81\\xa3", "[\\xe2\\x81\\xa3]", "\\xed\\xb0\\x80", "\\xed\\xa0\\x80",
97+
"(\\xed\\xb0\\x80)\\x01", "((\\xed\\xa0\\x80))\\x02", "\\xf0\\x9f\\x92\\xa9", "\\x01",
98+
"\\x0f", "[-\\xf0\\x9f\\x92\\xa9]+", "[\\xf0\\x9f\\x92\\xa9-\\xf4\\x8f\\xbf\\xbf]",
99+
"(?<=)", "(?<=a)", "(?<!)", "(?<!a)", "(?<a>)", "(?<a>.)",
100+
"(?<a>.)\\k<a>", "\\p{Script=Greek}", "\\P{sc=Greek}",
101+
"\\p{Script_Extensions=Greek}", "\\P{scx=Greek}",
102+
"\\p{General_Category=Decimal_Number}", "\\P{gc=Decimal_Number}",
103+
"\\p{gc=Nd}", "\\P{Decimal_Number}", "\\p{Nd}", "\\P{Any}",
104+
"\\p{Changes_When_NFKC_Casefolded}",
105+
"[\\p{Script_Extensions=Greek}--[α-γ]]",
106+
"[\\p{Script_Extensions=Mongolian}&&\\p{Number}]",
107+
"[\\q{abc|def|0|5}--\\d]"
108+
]
109+
53110
public let interestingRegExpQuantifiers = ["*", "+", "?"]
54111

55112
public let intType = JSType.integer

Sources/Fuzzilli/FuzzIL/JsOperations.swift

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,10 @@ public struct RegExpFlags: OptionSet, Hashable {
154154

155155
public func asString() -> String {
156156
var strRepr = ""
157+
158+
// These flags are mutually exclusive, will lead to runtime exceptions if used together
159+
assert(!(contains(.unicode) && contains(.unicodeSets)))
160+
157161
for (flag, char) in RegExpFlags.flagToCharDict {
158162
if contains(flag) {
159163
strRepr += char
@@ -178,22 +182,39 @@ public struct RegExpFlags: OptionSet, Hashable {
178182
flags.formUnion(.unicode)
179183
case "y":
180184
flags.formUnion(.sticky)
185+
case "d":
186+
flags.formUnion(.hasIndices)
187+
case "v":
188+
flags.formUnion(.unicodeSets)
181189
default:
182190
return nil
183191
}
184192
}
193+
// These flags are mutually exclusive, will lead to runtime exceptions if used together
194+
assert(!(flags.contains(.unicode) && flags.contains(.unicodeSets)))
185195
return flags
186196
}
187197

188-
static let caseInsensitive = RegExpFlags(rawValue: 1 << 0)
189-
static let global = RegExpFlags(rawValue: 1 << 1)
190-
static let multiline = RegExpFlags(rawValue: 1 << 2)
191-
static let dotall = RegExpFlags(rawValue: 1 << 3)
192-
static let unicode = RegExpFlags(rawValue: 1 << 4)
193-
static let sticky = RegExpFlags(rawValue: 1 << 5)
198+
static let caseInsensitive = RegExpFlags(rawValue: 1 << 0) // i
199+
static let global = RegExpFlags(rawValue: 1 << 1) // g
200+
static let multiline = RegExpFlags(rawValue: 1 << 2) // m
201+
static let dotall = RegExpFlags(rawValue: 1 << 3) // s
202+
static let unicode = RegExpFlags(rawValue: 1 << 4) // u
203+
static let sticky = RegExpFlags(rawValue: 1 << 5) // y
204+
static let hasIndices = RegExpFlags(rawValue: 1 << 6) // d
205+
static let unicodeSets = RegExpFlags(rawValue: 1 << 7) // v
194206

195207
public static func random() -> RegExpFlags {
196-
return RegExpFlags(rawValue: UInt32.random(in: 0..<(1<<6)))
208+
var flags = RegExpFlags(rawValue: UInt32.random(in: 0..<(1<<8)))
209+
if flags.contains(.unicode) && flags.contains(.unicodeSets) {
210+
// clear one of them as they are mutually exclusive, they will throw a runtime exception if used together.
211+
withEqualProbability({
212+
flags.subtract(.unicode)
213+
}, {
214+
flags.subtract(.unicodeSets)
215+
})
216+
}
217+
return flags
197218
}
198219

199220
private static let flagToCharDict: [RegExpFlags:String] = [
@@ -203,6 +224,8 @@ public struct RegExpFlags: OptionSet, Hashable {
203224
.dotall: "s",
204225
.unicode: "u",
205226
.sticky: "y",
227+
.hasIndices: "d",
228+
.unicodeSets: "v",
206229
]
207230
}
208231

Sources/FuzzilliCli/Profiles/V8Profile.swift

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,106 @@ fileprivate let MapTransitionsTemplate = ProgramTemplate("MapTransitionsTemplate
304304
}
305305
}
306306

307+
// This template fuzzes the RegExp engine.
308+
// It finds bugs like: crbug.com/1437346 and crbug.com/1439691.
309+
fileprivate let RegExpFuzzerTemplate = ProgramTemplate("RegExpFuzzerTemplate") { b in
310+
// Taken from: https://source.chromium.org/chromium/chromium/src/+/refs/heads/main:v8/test/fuzzer/regexp-builtins.cc;l=212;drc=a61b95c63b0b75c1cfe872d9c8cdf927c226046e
311+
let twoByteSubjectString = "f\\uD83D\\uDCA9ba\\u2603"
312+
313+
let replacementCandidates = [
314+
"'X'",
315+
"'$1$2$3'",
316+
"'$$$&$`$\\'$1'",
317+
"() => 'X'",
318+
"(arg0, arg1, arg2, arg3, arg4) => arg0 + arg1 + arg2 + arg3 + arg4",
319+
"() => 42"
320+
]
321+
322+
let lastIndices = [
323+
"undefined", "-1", "0",
324+
"1", "2", "3",
325+
"4", "5", "6",
326+
"7", "8", "9",
327+
"50", "4294967296", "2147483647",
328+
"2147483648", "NaN", "Not a Number"
329+
]
330+
331+
let f = b.buildPlainFunction(with: .parameters(n: 0)) { _ in
332+
let pattern = probability(0.5) ? chooseUniform(from: b.fuzzer.environment.interestingRegExps) : b.randomString()
333+
let regExpVar = b.loadRegExp(pattern, RegExpFlags.random())
334+
335+
let lastIndex = chooseUniform(from: lastIndices)
336+
let lastIndexString = b.loadString(lastIndex)
337+
338+
b.setProperty("lastIndex", of: regExpVar, to: lastIndexString)
339+
340+
let subjectVar: Variable
341+
342+
if probability(0.1) {
343+
subjectVar = b.loadString(twoByteSubjectString)
344+
} else {
345+
subjectVar = b.loadString(b.randomString())
346+
}
347+
348+
let resultVar = b.loadNull()
349+
350+
b.buildTryCatchFinally(tryBody: {
351+
let symbol = b.loadBuiltin("Symbol")
352+
withEqualProbability({
353+
let res = b.callMethod("exec", on: regExpVar, withArgs: [subjectVar])
354+
b.reassign(resultVar, to: res)
355+
}, {
356+
let prop = b.getProperty("match", of: symbol)
357+
let res = b.callComputedMethod(prop, on: regExpVar, withArgs: [subjectVar])
358+
b.reassign(resultVar, to: res)
359+
}, {
360+
let prop = b.getProperty("replace", of: symbol)
361+
let replacement = withEqualProbability({
362+
b.loadString(b.randomString())
363+
}, {
364+
b.loadString(chooseUniform(from: replacementCandidates))
365+
})
366+
let res = b.callComputedMethod(prop, on: regExpVar, withArgs: [subjectVar, replacement])
367+
b.reassign(resultVar, to: res)
368+
}, {
369+
let prop = b.getProperty("search", of: symbol)
370+
let res = b.callComputedMethod(prop, on: regExpVar, withArgs: [subjectVar])
371+
b.reassign(resultVar, to: res)
372+
}, {
373+
let prop = b.getProperty("split", of: symbol)
374+
let randomSplitLimit = withEqualProbability({
375+
"undefined"
376+
}, {
377+
"'not a number'"
378+
}, {
379+
String(b.randomInt())
380+
})
381+
let limit = b.loadString(randomSplitLimit)
382+
let res = b.callComputedMethod(symbol, on: regExpVar, withArgs: [subjectVar, limit])
383+
b.reassign(resultVar, to: res)
384+
}, {
385+
let res = b.callMethod("test", on: regExpVar, withArgs: [subjectVar])
386+
b.reassign(resultVar, to: res)
387+
})
388+
}, catchBody: { _ in
389+
})
390+
391+
b.build(n: 7)
392+
393+
b.doReturn(resultVar)
394+
}
395+
396+
b.eval("%SetForceSlowPath(false)");
397+
// compile the regexp once
398+
b.callFunction(f)
399+
let resFast = b.callFunction(f)
400+
b.eval("%SetForceSlowPath(true)");
401+
let resSlow = b.callFunction(f)
402+
b.eval("%SetForceSlowPath(false)");
403+
404+
b.build(n: 15)
405+
}
406+
307407
let v8Profile = Profile(
308408
processArgs: { randomize in
309409
var args = [
@@ -432,6 +532,7 @@ let v8Profile = Profile(
432532

433533
additionalProgramTemplates: WeightedList<ProgramTemplate>([
434534
(MapTransitionsTemplate, 1),
535+
(RegExpFuzzerTemplate, 1),
435536
]),
436537

437538
disabledCodeGenerators: [],

0 commit comments

Comments
 (0)