Repository: google/clusterfuzz

Issue:
For stack-overflows, don't use crash state similarity to close bugs. 
Stack overflows can shift frames very easily, leading to us incorrectly detecting bugs as fixed.


Discussion:

Resolving patch:
diff --git a/src/clusterfuzz/_internal/bot/testcase_manager.py b/src/clusterfuzz/_internal/bot/testcase_manager.py
--- a/src/clusterfuzz/_internal/bot/testcase_manager.py
+++ b/src/clusterfuzz/_internal/bot/testcase_manager.py
@@ -77,6 +77,11 @@
     'logging service has stopped',
 ]
 
+# Flaky crash types for which crash comparison does not make sense.
+IGNORE_STATE_CRASH_TYPES = [
+    'Stack-overflow',
+]
+
 
 class TestcaseManagerError(Exception):
   """Base exception."""
@@ -198,7 +203,7 @@ def read_resource_list(resource_file_path):
 
   resources = []
   base_directory = os.path.dirname(resource_file_path)
-  with open(resource_file_path) as file_handle:
+  with open(resource_file_path, encoding='utf-8') as file_handle:
     resource_file_contents = file_handle.read()
     for line in resource_file_contents.splitlines():
       resource = os.path.join(base_directory, line.strip())
@@ -773,7 +778,7 @@ def test_for_crash_with_retries(testcase,
     if crash_retries is None:
       crash_retries = environment.get_value('CRASH_RETRIES')
 
-    if compare_crash:
+    if compare_crash and testcase.crash_type not in IGNORE_STATE_CRASH_TYPES:
       expected_state = testcase.crash_state
       expected_security_flag = testcase.security_flag
     else:


Test patch:
diff --git a/src/clusterfuzz/_internal/tests/core/bot/testcase_manager_test.py b/src/clusterfuzz/_internal/tests/core/bot/testcase_manager_test.py
--- a/src/clusterfuzz/_internal/tests/core/bot/testcase_manager_test.py
+++ b/src/clusterfuzz/_internal/tests/core/bot/testcase_manager_test.py
@@ -64,7 +64,8 @@ def test_create(self):
         'cc.txt',
         'aa/dd.txt',
     ])
-    actual_files_list = set(open(testcase_list_file_path).read().splitlines())
+    actual_files_list = set(
+        open(testcase_list_file_path, encoding='utf-8').read().splitlines())
     self.assertEqual(expected_files_list, actual_files_list)
 
 
@@ -601,6 +602,44 @@ def test_test_for_crash_with_retries_blackbox_succeed_no_comparison(self):
         mock.call('Crash stacktrace comparison skipped.')
     ])
 
+  def test_test_for_crash_with_retries_blackbox_stack_overflow(self):
+    """Test test_for_crash_with_retries reproducing a Stack-overflow crash."""
+    self.mock.run_process.side_effect = [
+        (0, 0, 'output'),
+        (1, 1, 'crash'),
+    ]
+
+    self.blackbox_testcase.crash_type = 'Stack-overflow'
+    self.blackbox_testcase.put()
+
+    # compare_crash should be overridden to False.
+    crash_result = testcase_manager.test_for_crash_with_retries(
+        self.blackbox_testcase, '/fuzz-testcase', 10, compare_crash=True)
+    self.assertEqual(1, crash_result.return_code)
+    self.assertEqual(1, crash_result.crash_time)
+    self.assertEqual('crash', crash_result.output)
+    self.assertEqual(2, self.mock.run_process.call_count)
+
+    self.mock.run_process.assert_has_calls([
+        mock.call(
+            '/build_dir/app_name -arg1 -arg2',
+            current_working_directory='/build_dir',
+            gestures=[],
+            timeout=120),
+        mock.call(
+            '/build_dir/app_name -arg1 -arg2',
+            current_working_directory='/build_dir',
+            gestures=[],
+            timeout=10),
+    ])
+    self.mock.log.assert_has_calls([
+        mock.call('No crash occurred (round 1).', output='output'),
+        mock.call(
+            'Crash occurred in 1 seconds (round 2). State:\nstate',
+            output='crash'),
+        mock.call('Crash stacktrace comparison skipped.')
+    ])
+
   def test_test_for_crash_with_retries_greybox_succeed(self):
     """Test test_for_crash_with_retries reproducing a crash (greybox)."""
     mock_engine = mock.Mock()
@@ -672,7 +711,7 @@ def test_test_for_crash_with_retries_greybox_legacy(self):
     ]
     self.mock.get.return_value = mock_engine
 
-    with open('/flags-testcase', 'w') as f:
+    with open('/flags-testcase', 'w', encoding='utf-8') as f:
       f.write('%TESTCASE% target -arg1 -arg2')
 
     testcase_manager.test_for_crash_with_retries(self.greybox_testcase,
diff --git a/src/clusterfuzz/_internal/tests/core/local/butler/reproduce_test.py b/src/clusterfuzz/_internal/tests/core/local/butler/reproduce_test.py
--- a/src/clusterfuzz/_internal/tests/core/local/butler/reproduce_test.py
+++ b/src/clusterfuzz/_internal/tests/core/local/butler/reproduce_test.py
@@ -33,6 +33,7 @@
 def _fake_get_echo_testcase(*_):
   """Fake test case output intended to run "echo -n"."""
   testcase_map = {
+      'crash_type': 'type',
       'crash_state': 'state',
       'security_flag': False,
       'gestures': [],
@@ -58,6 +59,7 @@ def _fake_get_echo_testcase(*_):
 def _fake_get_libfuzzer_testcase(*_):
   """Fake test case output intended to run "echo -n"."""
   testcase_map = {
+      'crash_type': 'type',
       'crash_state': 'state',
       'security_flag': False,
       'gestures': [],
@@ -122,7 +124,7 @@ def test_reproduce_with_echo(self):
     self.mock._get_testcase.side_effect = _fake_get_echo_testcase
 
     binary_path = os.path.join(self.build_directory, 'echo')
-    with open(binary_path, 'w') as f:
+    with open(binary_path, 'w', encoding='utf-8') as f:
       f.write('test')
 
     crash_retries = 3