[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:
jonathanmetzman 2021-11-02 08:01:46 -04:00 committed by GitHub
parent 3f26615977
commit 7693e9640c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 112 additions and 19 deletions

View File

@ -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():

View File

@ -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).

View File

@ -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()

View File

@ -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.