mirror of https://github.com/google/oss-fuzz.git
[clusterfuzzlite] Make timeout and OOM reporting optional. (#6711)
OOMs will be reported by default. Timeouts wont. Fixes: https://github.com/google/oss-fuzz/issues/6703 Should also fix: https://github.com/google/oss-fuzz/issues/6619 Fixes: https://github.com/google/oss-fuzz/issues/3432 Related: https://github.com/google/oss-fuzz/issues/6685
This commit is contained in:
parent
3f26615977
commit
7693e9640c
|
@ -195,6 +195,9 @@ class RunFuzzersConfig(BaseConfig):
|
|||
self.report_unreproducible_crashes = environment.get_bool(
|
||||
'REPORT_UNREPRODUCIBLE_CRASHES', False)
|
||||
|
||||
self.report_timeouts = environment.get_bool('REPORT_TIMEOUTS', False)
|
||||
self.report_ooms = environment.get_bool('REPORT_OOMS', True)
|
||||
|
||||
# TODO(metzman): Fix tests to create valid configurations and get rid of
|
||||
# CIFUZZ_TEST here and in presubmit.py.
|
||||
if not os.getenv('CIFUZZ_TEST') and not self._run_config_validate():
|
||||
|
|
|
@ -169,8 +169,8 @@ class FuzzTarget: # pylint: disable=too-many-instance-attributes
|
|||
result = engine_impl.fuzz(self.target_path, options, artifacts_dir,
|
||||
self.duration)
|
||||
|
||||
# Libfuzzer timeout was reached.
|
||||
if not result.crashes:
|
||||
# Libfuzzer max time was reached.
|
||||
logging.info('Fuzzer %s finished with no crashes discovered.',
|
||||
self.target_name)
|
||||
return FuzzResult(None, None, self.latest_corpus_path)
|
||||
|
@ -179,7 +179,7 @@ class FuzzTarget: # pylint: disable=too-many-instance-attributes
|
|||
crash = result.crashes[0]
|
||||
logging.info('Fuzzer: %s. Detected bug.', self.target_name)
|
||||
|
||||
if self.is_crash_reportable(crash.input_path):
|
||||
if self.is_crash_reportable(crash.input_path, crash.reproduce_args):
|
||||
# We found a bug in the fuzz target and we will report it.
|
||||
saved_path = self._save_crash(crash)
|
||||
return FuzzResult(saved_path, result.logs, self.latest_corpus_path)
|
||||
|
@ -211,12 +211,14 @@ class FuzzTarget: # pylint: disable=too-many-instance-attributes
|
|||
os.remove(self.target_path)
|
||||
logging.info('Done deleting.')
|
||||
|
||||
def is_reproducible(self, testcase, target_path):
|
||||
def is_reproducible(self, testcase, target_path, reproduce_args):
|
||||
"""Checks if the testcase reproduces.
|
||||
|
||||
Args:
|
||||
testcase: The path to the testcase to be tested.
|
||||
target_path: The path to the fuzz target to be tested
|
||||
reproduce_args: The arguments to pass to the target to reproduce the
|
||||
crash.
|
||||
|
||||
Returns:
|
||||
True if crash is reproducible and we were able to run the
|
||||
|
@ -240,7 +242,7 @@ class FuzzTarget: # pylint: disable=too-many-instance-attributes
|
|||
engine_impl = clusterfuzz.fuzz.get_engine(config_utils.DEFAULT_ENGINE)
|
||||
result = engine_impl.reproduce(target_path,
|
||||
testcase,
|
||||
arguments=[],
|
||||
arguments=reproduce_args,
|
||||
max_time=REPRODUCE_TIME_SECONDS)
|
||||
|
||||
if result.return_code != 0:
|
||||
|
@ -253,13 +255,15 @@ class FuzzTarget: # pylint: disable=too-many-instance-attributes
|
|||
target_path)
|
||||
return False
|
||||
|
||||
def is_crash_reportable(self, testcase):
|
||||
def is_crash_reportable(self, testcase, reproduce_args):
|
||||
"""Returns True if a crash is reportable. This means the crash is
|
||||
reproducible but not reproducible on a build from the ClusterFuzz deployment
|
||||
(meaning the crash was introduced by this PR/commit/code change).
|
||||
|
||||
Args:
|
||||
testcase: The path to the testcase that triggered the crash.
|
||||
reproduce_args: The arguments to pass to the target to reproduce the
|
||||
crash.
|
||||
|
||||
Returns:
|
||||
True if the crash was introduced by the current pull request.
|
||||
|
@ -267,12 +271,16 @@ class FuzzTarget: # pylint: disable=too-many-instance-attributes
|
|||
Raises:
|
||||
ReproduceError if we can't attempt to reproduce the crash on the PR build.
|
||||
"""
|
||||
|
||||
if not self.is_crash_type_reportable(testcase):
|
||||
return False
|
||||
|
||||
if not os.path.exists(testcase):
|
||||
raise ReproduceError(f'Testcase {testcase} not found.')
|
||||
|
||||
try:
|
||||
reproducible_on_code_change = self.is_reproducible(
|
||||
testcase, self.target_path)
|
||||
testcase, self.target_path, reproduce_args)
|
||||
except ReproduceError as error:
|
||||
logging.error('Could not check for crash reproducibility.'
|
||||
'Please file an issue:'
|
||||
|
@ -284,9 +292,20 @@ class FuzzTarget: # pylint: disable=too-many-instance-attributes
|
|||
return self.config.report_unreproducible_crashes
|
||||
|
||||
logging.info('Crash is reproducible.')
|
||||
return self.is_crash_novel(testcase)
|
||||
return self.is_crash_novel(testcase, reproduce_args)
|
||||
|
||||
def is_crash_novel(self, testcase):
|
||||
def is_crash_type_reportable(self, testcase):
|
||||
"""Returns True if |testcase| is an actual crash. If crash is a timeout or
|
||||
OOM then returns True if config says we should report those."""
|
||||
# TODO(metzman): Use a less hacky method.
|
||||
testcase = os.path.basename(testcase)
|
||||
if testcase.startswith('oom-'):
|
||||
return self.config.report_ooms
|
||||
if testcase.startswith('timeout-'):
|
||||
return self.config.report_timeouts
|
||||
return True
|
||||
|
||||
def is_crash_novel(self, testcase, reproduce_args):
|
||||
"""Returns whether or not the crash is new. A crash is considered new if it
|
||||
can't be reproduced on an older ClusterFuzz build of the target."""
|
||||
if not os.path.exists(testcase):
|
||||
|
@ -303,7 +322,7 @@ class FuzzTarget: # pylint: disable=too-many-instance-attributes
|
|||
|
||||
try:
|
||||
reproducible_on_clusterfuzz_build = self.is_reproducible(
|
||||
testcase, clusterfuzz_target_path)
|
||||
testcase, clusterfuzz_target_path, reproduce_args)
|
||||
except ReproduceError:
|
||||
# This happens if the project has ClusterFuzz builds, but the fuzz target
|
||||
# is not in it (e.g. because the fuzz target is new).
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
"""Tests the functionality of the fuzz_target module."""
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import tempfile
|
||||
import unittest
|
||||
from unittest import mock
|
||||
|
@ -42,6 +43,8 @@ EXAMPLE_FUZZER = 'example_crash_fuzzer'
|
|||
EXECUTE_SUCCESS_RESULT = engine.ReproduceResult([], 0, 0, '')
|
||||
EXECUTE_FAILURE_RESULT = engine.ReproduceResult([], 1, 0, '')
|
||||
|
||||
TEST_DATA_PATH = os.path.join(os.path.dirname(__file__), 'test_data')
|
||||
|
||||
|
||||
def _create_config(**kwargs):
|
||||
"""Creates a config object and then sets every attribute that is a key in
|
||||
|
@ -100,7 +103,7 @@ class IsReproducibleTest(fake_filesystem_unittest.TestCase):
|
|||
mock_get_engine().reproduce.side_effect = all_repro
|
||||
|
||||
result = self.target.is_reproducible(self.testcase_path,
|
||||
self.fuzz_target_path)
|
||||
self.fuzz_target_path, [])
|
||||
mock_get_engine().reproduce.assert_called_once_with(
|
||||
'/workspace/build-out/fuzz-target',
|
||||
'/testcase',
|
||||
|
@ -116,8 +119,8 @@ class IsReproducibleTest(fake_filesystem_unittest.TestCase):
|
|||
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.target.is_reproducible(self.testcase_path, self.fuzz_target_path,
|
||||
[]))
|
||||
self.assertEqual(fuzz_target.REPRODUCE_ATTEMPTS,
|
||||
mock_get_engine().reproduce.call_count)
|
||||
|
||||
|
@ -125,7 +128,7 @@ class IsReproducibleTest(fake_filesystem_unittest.TestCase):
|
|||
"""Tests that is_reproducible raises an error if it could not attempt
|
||||
reproduction because the fuzzer doesn't exist."""
|
||||
with self.assertRaises(fuzz_target.ReproduceError):
|
||||
self.target.is_reproducible(self.testcase_path, '/non-existent-path')
|
||||
self.target.is_reproducible(self.testcase_path, '/non-existent-path', [])
|
||||
|
||||
def test_unreproducible(self, _):
|
||||
"""Tests that is_reproducible returns False for a crash that did not
|
||||
|
@ -134,7 +137,7 @@ class IsReproducibleTest(fake_filesystem_unittest.TestCase):
|
|||
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.fuzz_target_path, [])
|
||||
self.assertFalse(result)
|
||||
|
||||
|
||||
|
@ -167,7 +170,7 @@ class IsCrashReportableTest(fake_filesystem_unittest.TestCase):
|
|||
"""Tests that a new reproducible crash returns True."""
|
||||
with tempfile.TemporaryDirectory() as tmp_dir:
|
||||
self.target.out_dir = tmp_dir
|
||||
self.assertTrue(self.target.is_crash_reportable(self.testcase_path))
|
||||
self.assertTrue(self.target.is_crash_reportable(self.testcase_path, []))
|
||||
mock_info.assert_called_with(
|
||||
'The crash is not reproducible on previous build. '
|
||||
'Code change (pr/commit) introduced crash.')
|
||||
|
@ -191,7 +194,8 @@ class IsCrashReportableTest(fake_filesystem_unittest.TestCase):
|
|||
side_effect=is_reproducible_retvals):
|
||||
with mock.patch('clusterfuzz_deployment.OSSFuzz.download_latest_build',
|
||||
return_value=self.oss_fuzz_build_path):
|
||||
self.assertFalse(self.target.is_crash_reportable(self.testcase_path))
|
||||
self.assertFalse(self.target.is_crash_reportable(
|
||||
self.testcase_path, []))
|
||||
|
||||
@mock.patch('logging.info')
|
||||
@mock.patch('fuzz_target.FuzzTarget.is_reproducible', return_value=[True])
|
||||
|
@ -201,7 +205,9 @@ class IsCrashReportableTest(fake_filesystem_unittest.TestCase):
|
|||
is new)."""
|
||||
os.remove(self.oss_fuzz_target_path)
|
||||
|
||||
def is_reproducible_side_effect(_, target_path):
|
||||
def is_reproducible_side_effect(testcase, target_path, reproduce_arguments):
|
||||
del testcase
|
||||
del reproduce_arguments
|
||||
if os.path.dirname(target_path) == self.oss_fuzz_build_path:
|
||||
raise fuzz_target.ReproduceError()
|
||||
return True
|
||||
|
@ -211,9 +217,9 @@ class IsCrashReportableTest(fake_filesystem_unittest.TestCase):
|
|||
side_effect=is_reproducible_side_effect) as mock_is_reproducible:
|
||||
with mock.patch('clusterfuzz_deployment.OSSFuzz.download_latest_build',
|
||||
return_value=self.oss_fuzz_build_path):
|
||||
self.assertTrue(self.target.is_crash_reportable(self.testcase_path))
|
||||
self.assertTrue(self.target.is_crash_reportable(self.testcase_path, []))
|
||||
mock_is_reproducible.assert_any_call(self.testcase_path,
|
||||
self.oss_fuzz_target_path)
|
||||
self.oss_fuzz_target_path, [])
|
||||
mock_info.assert_called_with(
|
||||
'Could not run previous build of target to determine if this code '
|
||||
'change (pr/commit) introduced crash. Assuming crash was newly '
|
||||
|
@ -240,5 +246,34 @@ class FuzzTest(fake_filesystem_unittest.TestCase):
|
|||
fuzz_target_artifact)
|
||||
|
||||
|
||||
class TimeoutIntegrationTest(unittest.TestCase):
|
||||
"""Tests handling of fuzzer timeout (timeout crashes reported by
|
||||
libFuzzer)."""
|
||||
TIMEOUT_FUZZER_NAME = 'timeout_fuzzer'
|
||||
|
||||
@parameterized.parameterized.expand([(True, True), (False, False)])
|
||||
def test_timeout_reported(self, report_timeouts, expect_crash):
|
||||
"""Tests that timeouts are not reported."""
|
||||
with test_helpers.temp_dir_copy(TEST_DATA_PATH) as temp_dir:
|
||||
fuzz_target_path = os.path.join(temp_dir, 'build-out',
|
||||
self.TIMEOUT_FUZZER_NAME)
|
||||
shutil.copy(os.path.join(temp_dir, self.TIMEOUT_FUZZER_NAME),
|
||||
fuzz_target_path)
|
||||
deployment = _create_deployment(workspace=temp_dir,
|
||||
report_timeouts=report_timeouts)
|
||||
config = deployment.config
|
||||
fuzz_target_obj = fuzz_target.FuzzTarget(fuzz_target_path,
|
||||
fuzz_target.REPRODUCE_ATTEMPTS,
|
||||
deployment.workspace, deployment,
|
||||
config)
|
||||
with mock.patch('clusterfuzz._internal.bot.fuzzers.libfuzzer.'
|
||||
'fix_timeout_argument_for_reproduction') as _:
|
||||
with mock.patch(
|
||||
'clusterfuzz._internal.bot.fuzzers.libFuzzer.fuzzer.get_arguments',
|
||||
return_value=['-timeout=1', '-rss_limit_mb=2560']):
|
||||
fuzz_result = fuzz_target_obj.fuzz()
|
||||
self.assertEqual(bool(fuzz_result.testcase), expect_crash)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
// Copyright 2021 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Example of a standalone runner for "fuzz targets".
|
||||
// It reads all files passed as parameters and feeds their contents
|
||||
// one by one into the fuzz target (LLVMFuzzerTestOneInput).
|
||||
// This runner does not do any fuzzing, but allows us to run the fuzz target
|
||||
// on the test corpus (e.g. "do_stuff_test_data") or on a single file,
|
||||
// e.g. the one that comes from a bug report.
|
||||
|
||||
// This is a fuzz target that times out on every input by infinite looping.
|
||||
// This is used for testing.
|
||||
// Build instructions:
|
||||
// 1. clang++ -fsanitize=fuzzer TimeoutFuzzer.cpp -o timeout_fuzzer
|
||||
// 2. strip timeout_fuzzer
|
||||
// The binary is stripped to save space in the git repo.
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
extern "C" int LLVMFuzzerTestOneInput(uint8_t* data, size_t size) {
|
||||
while (true)
|
||||
;
|
||||
return 0;
|
||||
}
|
Binary file not shown.
Loading…
Reference in New Issue