Use libClusterFuzz for reproduction. (#6495)

Fixes #6326.
This commit is contained in:
Oliver Chang 2021-09-24 15:46:13 +10:00 committed by GitHub
parent aa8740a98a
commit 525e9eccd0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 46 additions and 43 deletions

View File

@ -17,13 +17,8 @@ import logging
import os
import shutil
import stat
import sys
import base_runner_utils
import config_utils
# pylint: disable=wrong-import-position,import-error
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import utils
import clusterfuzz.environment
import clusterfuzz.fuzz
@ -39,6 +34,8 @@ LIBFUZZER_OPTIONS = ['-seed=1337', '-len_control=0']
# The number of reproduce attempts for a crash.
REPRODUCE_ATTEMPTS = 10
REPRODUCE_TIME_SECONDS = 30
# Seconds on top of duration until a timeout error is raised.
BUFFER_TIME = 10
@ -213,21 +210,25 @@ class FuzzTarget: # pylint: disable=too-many-instance-attributes
os.chmod(target_path, stat.S_IRWXO)
env = base_runner_utils.get_env(self.config, self.workspace)
env['TESTCASE'] = testcase
command = ['reproduce', self.target_name, '-runs=100']
logging.info('Trying to reproduce crash using: %s.', testcase)
with clusterfuzz.environment.Environment(config_utils.DEFAULT_ENGINE,
self.config.sanitizer,
target_path,
interactive=True):
for _ in range(REPRODUCE_ATTEMPTS):
engine_impl = clusterfuzz.fuzz.get_engine(config_utils.DEFAULT_ENGINE)
result = engine_impl.reproduce(target_path,
testcase,
arguments=[],
max_time=REPRODUCE_TIME_SECONDS)
logging.info('Running reproduce command: %s.', ' '.join(command))
for _ in range(REPRODUCE_ATTEMPTS):
_, _, returncode = utils.execute(command, env=env)
if result.return_code != 0:
logging.info('Reproduce command returned: %s. Reproducible on %s.',
result.return_code, target_path)
if returncode != 0:
logging.info('Reproduce command returned: %s. Reproducible on %s.',
returncode, target_path)
return True
return True
logging.info('Reproduce command returned 0. Not reproducible on %s.',
logging.info('Reproduce command returned: 0. Not reproducible on %s.',
target_path)
return False

View File

@ -19,8 +19,12 @@ import unittest
from unittest import mock
import certifi
# Importing this later causes import failures with pytest for some reason.
# TODO(ochang): Figure out why.
import parameterized
import google.cloud.ndb # pylint: disable=unused-import
from pyfakefs import fake_filesystem_unittest
from clusterfuzz.fuzz import engine
import clusterfuzz_deployment
import fuzz_target
@ -34,11 +38,9 @@ EXAMPLE_PROJECT = 'example'
# An example fuzzer that triggers an error.
EXAMPLE_FUZZER = 'example_crash_fuzzer'
# The return value of a successful call to utils.execute.
EXECUTE_SUCCESS_RETVAL = ('', '', 0)
# The return value of a failed call to utils.execute.
EXECUTE_FAILURE_RETVAL = ('', '', 1)
# Mock return values for engine_impl.reproduce.
EXECUTE_SUCCESS_RESULT = engine.ReproduceResult([], 0, 0, '')
EXECUTE_FAILURE_RESULT = engine.ReproduceResult([], 1, 0, '')
def _create_config(**kwargs):
@ -85,40 +87,39 @@ class IsReproducibleTest(fake_filesystem_unittest.TestCase):
self.workspace, deployment,
deployment.config)
# ClusterFuzz requires ROOT_DIR.
root_dir = os.environ['ROOT_DIR']
test_helpers.patch_environ(self, empty=True)
os.environ['ROOT_DIR'] = root_dir
def test_reproducible(self, _):
"""Tests that is_reproducible returns True if crash is detected and that
is_reproducible uses the correct command to reproduce a crash."""
all_repro = [EXECUTE_FAILURE_RETVAL] * fuzz_target.REPRODUCE_ATTEMPTS
with mock.patch('utils.execute', side_effect=all_repro) as mock_execute:
all_repro = [EXECUTE_FAILURE_RESULT] * fuzz_target.REPRODUCE_ATTEMPTS
with mock.patch('clusterfuzz.fuzz.get_engine') as mock_get_engine:
mock_get_engine().reproduce.side_effect = all_repro
result = self.target.is_reproducible(self.testcase_path,
self.fuzz_target_path)
expected_command = ['reproduce', 'fuzz-target', '-runs=100']
expected_env = {
'SANITIZER': self.config.sanitizer,
'FUZZING_LANGUAGE': 'c++',
'OUT': self.workspace.out,
'CIFUZZ': 'True',
'FUZZING_ENGINE': 'libfuzzer',
'ARCHITECTURE': 'x86_64',
'TESTCASE': self.testcase_path,
'FUZZER_ARGS': '-rss_limit_mb=2560 -timeout=25'
}
mock_execute.assert_called_once_with(expected_command, env=expected_env)
mock_get_engine().reproduce.assert_called_once_with(
'/workspace/build-out/fuzz-target',
'/testcase',
arguments=[],
max_time=30)
self.assertTrue(result)
self.assertEqual(1, mock_execute.call_count)
self.assertEqual(1, mock_get_engine().reproduce.call_count)
def test_flaky(self, _):
"""Tests that is_reproducible returns True if crash is detected on the last
attempt."""
last_time_repro = [EXECUTE_SUCCESS_RETVAL] * 9 + [EXECUTE_FAILURE_RETVAL]
with mock.patch('utils.execute',
side_effect=last_time_repro) as mock_execute:
last_time_repro = [EXECUTE_SUCCESS_RESULT] * 9 + [EXECUTE_FAILURE_RESULT]
with mock.patch('clusterfuzz.fuzz.get_engine') as mock_get_engine:
mock_get_engine().reproduce.side_effect = last_time_repro
self.assertTrue(
self.target.is_reproducible(self.testcase_path,
self.fuzz_target_path))
self.assertEqual(fuzz_target.REPRODUCE_ATTEMPTS, mock_execute.call_count)
self.assertEqual(fuzz_target.REPRODUCE_ATTEMPTS,
mock_get_engine().reproduce.call_count)
def test_nonexistent_fuzzer(self, _):
"""Tests that is_reproducible raises an error if it could not attempt
@ -129,8 +130,9 @@ class IsReproducibleTest(fake_filesystem_unittest.TestCase):
def test_unreproducible(self, _):
"""Tests that is_reproducible returns False for a crash that did not
reproduce."""
all_unrepro = [EXECUTE_SUCCESS_RETVAL] * fuzz_target.REPRODUCE_ATTEMPTS
with mock.patch('utils.execute', side_effect=all_unrepro):
all_unrepro = [EXECUTE_SUCCESS_RESULT] * fuzz_target.REPRODUCE_ATTEMPTS
with mock.patch('clusterfuzz.fuzz.get_engine') as mock_get_engine:
mock_get_engine().reproduce.side_effect = all_unrepro
result = self.target.is_reproducible(self.testcase_path,
self.fuzz_target_path)
self.assertFalse(result)