Repository: google/clusterfuzz

Issue:
Parse Jazzer.js stack traces
OSS-Fuzz now has support for fuzzing JavaScript/TypeScript code using Jazzer.js. ClusterFuzz needs to parse the corresponding stack traces to provide good findings reports. Similar to libFuzzer, Jazzer.js reports start with `==<PID>==<error type>: Jazzer.js: <error details>` followed by a stack trace. Here are two examples from Jazzer.js output when run from OSS-Fuzz:

# JavaScript
```shell
INFO: Running with entropic power schedule (0xFF, 100).
INFO: Seed: 1994432650
INFO: Loaded 1 modules   (512 inline 8-bit counters): 512 [0x7f049c22e010, 0x7f049c22e210), 
INFO: Loaded 1 PC tables (512 PCs): 512 [0x7f04957fc010,0x7f04957fe010), 
INFO:        0 files found in /tmp/fuzz_string_compare_corpus
INFO: -max_len is not provided; libFuzzer will not generate inputs larger than 4096 bytes
INFO: A corpus is not provided, starting from an empty corpus
#2	INITED cov: 2 ft: 2 corp: 1/1b exec/s: 0 rss: 122Mb
#1333	NEW    cov: 6 ft: 6 corp: 2/17b lim: 17 exec/s: 0 rss: 122Mb L: 16/16 MS: 1 InsertRepeatedBytes-
#17042	NEW    cov: 7 ft: 7 corp: 3/33b lim: 170 exec/s: 0 rss: 124Mb L: 16/16 MS: 4 ShuffleBytes-ChangeBit-ChangeByte-CMP- DE: "Awesome "-
#64199	NEW    cov: 8 ft: 8 corp: 4/49b lim: 634 exec/s: 0 rss: 142Mb L: 16/16 MS: 2 ChangeBit-CMP- DE: "Fuzzing"-
==14== Uncaught Exception: Jazzer.js: Welcome to Awesome Fuzzing!
Error: Welcome to Awesome Fuzzing!
    at check (/out/example/fuzz_string_compare.js:27:9)
    at module.exports.fuzz (/out/example/fuzz_string_compare.js:36:2)
    at /out/example/node_modules/@jazzer.js/core/core.ts:112:38
MS: 1 ChangeByte-; base unit: 24b3fc682a0f5246a6e58912bfa9386924c2fd9f
0x41,0x77,0x65,0x73,0x6f,0x6d,0x65,0x20,0x46,0x75,0x7a,0x7a,0x69,0x6e,0x67,0x21,
Awesome Fuzzing!
artifact_prefix='./'; Test unit written to ./crash-dd107abbf60f67c533ff7aecb116ce483fc4facf
Base64: QXdlc29tZSBGdXp6aW5nIQ==
```

# TypeScript
```shell
INFO: Running with entropic power schedule (0xFF, 100).
INFO: Seed: 2855137205
INFO: Loaded 1 modules   (512 inline 8-bit counters): 512 [0x7f161ac4b010, 0x7f161ac4b210), 
INFO: Loaded 1 PC tables (512 PCs): 512 [0x7f1619c4a010,0x7f1619c4c010), 
INFO:        0 files found in /tmp/fuzz_explore_me_corpus
INFO: -max_len is not provided; libFuzzer will not generate inputs larger than 4096 bytes
INFO: A corpus is not provided, starting from an empty corpus
#2	INITED cov: 4 ft: 4 corp: 1/1b exec/s: 0 rss: 126Mb
#5	NEW    cov: 5 ft: 5 corp: 2/3b lim: 4 exec/s: 0 rss: 126Mb L: 2/2 MS: 3 ShuffleBytes-ChangeByte-CrossOver-
#217	NEW    cov: 7 ft: 7 corp: 3/9b lim: 6 exec/s: 0 rss: 126Mb L: 6/6 MS: 2 CopyPart-InsertRepeatedBytes-
#1048576	pulse  cov: 7 ft: 7 corp: 3/9b lim: 4096 exec/s: 524288 rss: 153Mb
==14== Uncaught Exception: Jazzer.js: Crash!
Error: Crash!
    at exploreMe (/out/example/target.ts:6:15)
    at fuzz (/out/example/fuzz_explore_me.ts:22:11)
    at /out/example/node_modules/@jazzer.js/core/core.ts:112:38
MS: 3 ChangeBit-CopyPart-CMP- DE: "Hello World!"-; base unit: b17e7aebda507cae0ecf9b6a4f76d41d2ffc2d91
0x48,0x65,0x6c,0x6c,0x6f,0x20,0x57,0x6f,0x72,0x6c,0x64,0x21,0xa,0xed,0xec,0xec,0xed,0xed,0xed,0xed,
Hello World!\012\355\354\354\355\355\355\355
artifact_prefix='./'; Test unit written to ./crash-206c128d399a39ca72c63bb3605ae1536823a589
Base64: SGVsbG8gV29ybGQhCu3s7O3t7e0=
```


Discussion:

Resolving patch:
diff --git a/src/clusterfuzz/stacktraces/__init__.py b/src/clusterfuzz/stacktraces/__init__.py
--- a/src/clusterfuzz/stacktraces/__init__.py
+++ b/src/clusterfuzz/stacktraces/__init__.py
@@ -63,6 +63,7 @@ def __init__(self):
     self.is_lkl = False
     self.is_golang = False
     self.is_python = False
+    self.is_js = False
     self.found_python_crash = False
     self.found_golang_crash = False
     self.found_android_kernel_crash = False
@@ -397,6 +398,7 @@ def parse(self, stacktrace: str) -> CrashInfo:
     state.is_lkl = lkl_constants.LINUX_KERNEL_MODULE_STACK_TRACE in stacktrace
     state.is_golang = '.go:' in stacktrace
     state.is_python = '.py", line' in stacktrace
+    state.is_js = 'Uncaught Exception: Jazzer.js' in stacktrace
 
     # For Android LKL (and potentially kernel output), the KASAN crash may start
     # with the time since boot.  We need to remove this so that our regexes
@@ -629,6 +631,17 @@ def parse(self, stacktrace: str) -> CrashInfo:
             state.frame_count = 0
             continue
 
+      if state.is_js:
+        if self.update_state_on_match(
+            JAZZER_JS_UNCAUGHT_EXCEPTION,
+            line,
+            state,
+            new_type='Uncaught exception'):
+          state.found_python_crash = True
+          state.crash_state = ''
+          state.frame_count = 0
+          continue
+
       # Sanitizer SEGV crashes.
       segv_match = SAN_SEGV_REGEX.match(line)
       if segv_match:
@@ -1261,6 +1274,10 @@ def parse(self, stacktrace: str) -> CrashInfo:
           PYTHON_STACK_FRAME_FUNCTION_REGEX, line, state, group=3):
         continue
 
+      if state.is_js and self.add_frame_on_match(
+          JAZZER_JS_STACK_FRAME_FUNCTION_REGEX, line, state, group=1):
+        continue
+
     # Detect cycles in stack overflow bugs and update crash state.
     update_crash_state_for_stack_overflow_if_needed(state)
 
diff --git a/src/clusterfuzz/stacktraces/constants.py b/src/clusterfuzz/stacktraces/constants.py
--- a/src/clusterfuzz/stacktraces/constants.py
+++ b/src/clusterfuzz/stacktraces/constants.py
@@ -346,6 +346,13 @@
     #  File "<embedded stdlib>/gzip.py", line 421, in _read_gzip_header
     r'^\s*File "([^"]+)", line (\d+), in (.+)$')
 
+JAZZER_JS_UNCAUGHT_EXCEPTION = re.compile(
+    r'^\s*==\d+== Uncaught Exception: Jazzer.js: .*')
+
+JAZZER_JS_STACK_FRAME_FUNCTION_REGEX = re.compile(
+    #    at exploreMe (/out/example/target.ts:6:15)
+    r'^\s* at ([^\s]+) \(.*:\d+:\d+\)')
+
 # Mappings of Android kernel error status codes to strings.
 ANDROID_KERNEL_STATUS_TO_STRING = {
     0b0001: 'Alignment Fault',


Test patch:
diff --git a/src/clusterfuzz/_internal/tests/core/crash_analysis/stack_parsing/stack_analyzer_data/jazzer_js_javascript.txt b/src/clusterfuzz/_internal/tests/core/crash_analysis/stack_parsing/stack_analyzer_data/jazzer_js_javascript.txt
new file mode 100644
--- /dev/null
+++ b/src/clusterfuzz/_internal/tests/core/crash_analysis/stack_parsing/stack_analyzer_data/jazzer_js_javascript.txt
@@ -0,0 +1,21 @@
+INFO: Running with entropic power schedule (0xFF, 100).
+INFO: Seed: 1994432650
+INFO: Loaded 1 modules   (512 inline 8-bit counters): 512 [0x7f049c22e010, 0x7f049c22e210), 
+INFO: Loaded 1 PC tables (512 PCs): 512 [0x7f04957fc010,0x7f04957fe010), 
+INFO:        0 files found in /tmp/fuzz_string_compare_corpus
+INFO: -max_len is not provided; libFuzzer will not generate inputs larger than 4096 bytes
+INFO: A corpus is not provided, starting from an empty corpus
+#2	INITED cov: 2 ft: 2 corp: 1/1b exec/s: 0 rss: 122Mb
+#1333	NEW    cov: 6 ft: 6 corp: 2/17b lim: 17 exec/s: 0 rss: 122Mb L: 16/16 MS: 1 InsertRepeatedBytes-
+#17042	NEW    cov: 7 ft: 7 corp: 3/33b lim: 170 exec/s: 0 rss: 124Mb L: 16/16 MS: 4 ShuffleBytes-ChangeBit-ChangeByte-CMP- DE: "Awesome "-
+#64199	NEW    cov: 8 ft: 8 corp: 4/49b lim: 634 exec/s: 0 rss: 142Mb L: 16/16 MS: 2 ChangeBit-CMP- DE: "Fuzzing"-
+==14== Uncaught Exception: Jazzer.js: Welcome to Awesome Fuzzing!
+Error: Welcome to Awesome Fuzzing!
+    at check (/out/example/fuzz_string_compare.js:27:9)
+    at module.exports.fuzz (/out/example/fuzz_string_compare.js:36:2)
+    at /out/example/node_modules/@jazzer.js/core/core.ts:112:38
+MS: 1 ChangeByte-; base unit: 24b3fc682a0f5246a6e58912bfa9386924c2fd9f
+0x41,0x77,0x65,0x73,0x6f,0x6d,0x65,0x20,0x46,0x75,0x7a,0x7a,0x69,0x6e,0x67,0x21,
+Awesome Fuzzing!
+artifact_prefix='./'; Test unit written to ./crash-dd107abbf60f67c533ff7aecb116ce483fc4facf
+Base64: QXdlc29tZSBGdXp6aW5nIQ==
diff --git a/src/clusterfuzz/_internal/tests/core/crash_analysis/stack_parsing/stack_analyzer_data/jazzer_js_typescript.txt b/src/clusterfuzz/_internal/tests/core/crash_analysis/stack_parsing/stack_analyzer_data/jazzer_js_typescript.txt
new file mode 100644
--- /dev/null
+++ b/src/clusterfuzz/_internal/tests/core/crash_analysis/stack_parsing/stack_analyzer_data/jazzer_js_typescript.txt
@@ -0,0 +1,21 @@
+INFO: Running with entropic power schedule (0xFF, 100).
+INFO: Seed: 2855137205
+INFO: Loaded 1 modules   (512 inline 8-bit counters): 512 [0x7f161ac4b010, 0x7f161ac4b210), 
+INFO: Loaded 1 PC tables (512 PCs): 512 [0x7f1619c4a010,0x7f1619c4c010), 
+INFO:        0 files found in /tmp/fuzz_explore_me_corpus
+INFO: -max_len is not provided; libFuzzer will not generate inputs larger than 4096 bytes
+INFO: A corpus is not provided, starting from an empty corpus
+#2	INITED cov: 4 ft: 4 corp: 1/1b exec/s: 0 rss: 126Mb
+#5	NEW    cov: 5 ft: 5 corp: 2/3b lim: 4 exec/s: 0 rss: 126Mb L: 2/2 MS: 3 ShuffleBytes-ChangeByte-CrossOver-
+#217	NEW    cov: 7 ft: 7 corp: 3/9b lim: 6 exec/s: 0 rss: 126Mb L: 6/6 MS: 2 CopyPart-InsertRepeatedBytes-
+#1048576	pulse  cov: 7 ft: 7 corp: 3/9b lim: 4096 exec/s: 524288 rss: 153Mb
+==14== Uncaught Exception: Jazzer.js: Crash!
+Error: Crash!
+    at exploreMe (/out/example/target.ts:6:15)
+    at fuzz (/out/example/fuzz_explore_me.ts:22:11)
+    at /out/example/node_modules/@jazzer.js/core/core.ts:112:38
+MS: 3 ChangeBit-CopyPart-CMP- DE: "Hello World!"-; base unit: b17e7aebda507cae0ecf9b6a4f76d41d2ffc2d91
+0x48,0x65,0x6c,0x6c,0x6f,0x20,0x57,0x6f,0x72,0x6c,0x64,0x21,0xa,0xed,0xec,0xec,0xed,0xed,0xed,0xed,
+Hello World!\012\355\354\354\355\355\355\355
+artifact_prefix='./'; Test unit written to ./crash-206c128d399a39ca72c63bb3605ae1536823a589
+Base64: SGVsbG8gV29ybGQhCu3s7O3t7e0=
diff --git a/src/clusterfuzz/_internal/tests/core/crash_analysis/stack_parsing/stack_analyzer_test.py b/src/clusterfuzz/_internal/tests/core/crash_analysis/stack_parsing/stack_analyzer_test.py
--- a/src/clusterfuzz/_internal/tests/core/crash_analysis/stack_parsing/stack_analyzer_test.py
+++ b/src/clusterfuzz/_internal/tests/core/crash_analysis/stack_parsing/stack_analyzer_test.py
@@ -3504,3 +3504,27 @@ def test_split_stacktrace(self):
     expected_split_stacktrace = corrected_stacktrace.splitlines()
 
     self.assertEqual(actual_split_stacktrace, expected_split_stacktrace)
+
+  def test_jazzer_js_javascript(self):
+    """Test Jazzer.js JS stacktrace."""
+    data = self._read_test_data('jazzer_js_javascript.txt')
+    expected_type = 'Uncaught exception'
+    expected_state = ('check\nmodule.exports.fuzz\n')
+    expected_address = ''
+    expected_stacktrace = data
+    expected_security_flag = False
+    self._validate_get_crash_data(data, expected_type, expected_address,
+                                  expected_state, expected_stacktrace,
+                                  expected_security_flag)
+
+  def test_jazzer_js_typescript(self):
+    """Test Jazzer.js TS stacktrace."""
+    data = self._read_test_data('jazzer_js_typescript.txt')
+    expected_type = 'Uncaught exception'
+    expected_state = ('exploreMe\nfuzz\n')
+    expected_address = ''
+    expected_stacktrace = data
+    expected_security_flag = False
+    self._validate_get_crash_data(data, expected_type, expected_address,
+                                  expected_state, expected_stacktrace,
+                                  expected_security_flag)