From 525e9eccd0d50a1c879c729dbdfda667f68038ad Mon Sep 17 00:00:00 2001 From: Oliver Chang Date: Fri, 24 Sep 2021 15:46:13 +1000 Subject: [PATCH] Use libClusterFuzz for reproduction. (#6495) Fixes #6326. --- infra/cifuzz/fuzz_target.py | 35 +++++++++++---------- infra/cifuzz/fuzz_target_test.py | 54 +++++++++++++++++--------------- 2 files changed, 46 insertions(+), 43 deletions(-) diff --git a/infra/cifuzz/fuzz_target.py b/infra/cifuzz/fuzz_target.py index 85486b0b9..2a6936b0e 100644 --- a/infra/cifuzz/fuzz_target.py +++ b/infra/cifuzz/fuzz_target.py @@ -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 diff --git a/infra/cifuzz/fuzz_target_test.py b/infra/cifuzz/fuzz_target_test.py index 3eafdc497..ecea6fbbf 100644 --- a/infra/cifuzz/fuzz_target_test.py +++ b/infra/cifuzz/fuzz_target_test.py @@ -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)