mirror of https://github.com/google/oss-fuzz.git
[CIFuzz] Don't make everything a subdirectory of /out (#5970)
Use different subdirectories of workspace for builds, old builds, coverage reports, corpora and artifacts/testscases.
This commit is contained in:
parent
a9c49afb78
commit
0672aa4e1f
|
@ -30,15 +30,15 @@ EXAMPLE_PROJECT = 'example'
|
|||
|
||||
EXAMPLE_FILE_CHANGED = 'test.txt'
|
||||
|
||||
TEST_DATA_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)),
|
||||
'test_data')
|
||||
TEST_DATA_OUT_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)),
|
||||
'test_data', 'build-out')
|
||||
|
||||
|
||||
class RemoveUnaffectedFuzzTargets(unittest.TestCase):
|
||||
"""Tests remove_unaffected_fuzzers."""
|
||||
|
||||
TEST_FUZZER_1 = os.path.join(TEST_DATA_PATH, 'out', 'example_crash_fuzzer')
|
||||
TEST_FUZZER_2 = os.path.join(TEST_DATA_PATH, 'out', 'example_nocrash_fuzzer')
|
||||
TEST_FUZZER_1 = os.path.join(TEST_DATA_OUT_PATH, 'example_crash_fuzzer')
|
||||
TEST_FUZZER_2 = os.path.join(TEST_DATA_OUT_PATH, 'example_nocrash_fuzzer')
|
||||
|
||||
# yapf: disable
|
||||
@parameterized.parameterized.expand([
|
||||
|
|
|
@ -50,10 +50,9 @@ class Builder: # pylint: disable=too-many-instance-attributes
|
|||
def __init__(self, config, ci_system):
|
||||
self.config = config
|
||||
self.ci_system = ci_system
|
||||
self.out_dir = os.path.join(config.workspace, 'out')
|
||||
os.makedirs(self.out_dir, exist_ok=True)
|
||||
self.work_dir = os.path.join(config.workspace, 'work')
|
||||
os.makedirs(self.work_dir, exist_ok=True)
|
||||
self.workspace = docker.Workspace(config)
|
||||
self.workspace.initialize_dir(self.workspace.out)
|
||||
self.workspace.initialize_dir(self.workspace.work)
|
||||
self.image_repo_path = None
|
||||
self.host_repo_path = None
|
||||
self.repo_manager = None
|
||||
|
@ -75,13 +74,14 @@ class Builder: # pylint: disable=too-many-instance-attributes
|
|||
"""Moves the source code we want to fuzz into the project builder and builds
|
||||
the fuzzers from that source code. Returns True on success."""
|
||||
docker_args, docker_container = docker.get_base_docker_run_args(
|
||||
self.out_dir, self.config.sanitizer, self.config.language)
|
||||
self.workspace, self.config.sanitizer, self.config.language)
|
||||
if not docker_container:
|
||||
docker_args.extend(
|
||||
_get_docker_build_fuzzers_args_not_container(self.host_repo_path))
|
||||
|
||||
if self.config.sanitizer == 'memory':
|
||||
docker_args.extend(_get_docker_build_fuzzers_args_msan(self.work_dir))
|
||||
docker_args.extend(
|
||||
_get_docker_build_fuzzers_args_msan(self.workspace.work))
|
||||
self.handle_msan_prebuild(docker_container)
|
||||
|
||||
docker_args.extend([
|
||||
|
@ -111,7 +111,7 @@ class Builder: # pylint: disable=too-many-instance-attributes
|
|||
"""Post-build step for MSAN builds. Patches the build to use MSAN
|
||||
libraries."""
|
||||
helper.docker_run([
|
||||
'--volumes-from', container, '-e', f'WORK={self.work_dir}',
|
||||
'--volumes-from', container, '-e', f'WORK={self.workspace.work}',
|
||||
docker.MSAN_LIBS_BUILDER_TAG, 'patch_build.py', '/out'
|
||||
])
|
||||
|
||||
|
@ -121,7 +121,7 @@ class Builder: # pylint: disable=too-many-instance-attributes
|
|||
logging.info('Copying MSAN libs.')
|
||||
helper.docker_run([
|
||||
'--volumes-from', container, docker.MSAN_LIBS_BUILDER_TAG, 'bash', '-c',
|
||||
f'cp -r /msan {self.work_dir}'
|
||||
f'cp -r /msan {self.workspace.work}'
|
||||
])
|
||||
|
||||
def build(self):
|
||||
|
@ -146,7 +146,7 @@ class Builder: # pylint: disable=too-many-instance-attributes
|
|||
changed_files = self.ci_system.get_changed_code_under_test(
|
||||
self.repo_manager)
|
||||
affected_fuzz_targets.remove_unaffected_fuzz_targets(
|
||||
self.config.project_name, self.out_dir, changed_files,
|
||||
self.config.project_name, self.workspace.out, changed_files,
|
||||
self.image_repo_path)
|
||||
return True
|
||||
|
||||
|
@ -178,7 +178,7 @@ def build_fuzzers(config):
|
|||
return builder.build()
|
||||
|
||||
|
||||
def check_fuzzer_build(out_dir,
|
||||
def check_fuzzer_build(workspace,
|
||||
sanitizer,
|
||||
language,
|
||||
allowed_broken_targets_percentage=None):
|
||||
|
@ -191,14 +191,15 @@ def check_fuzzer_build(out_dir,
|
|||
Returns:
|
||||
True if fuzzers are correct.
|
||||
"""
|
||||
if not os.path.exists(out_dir):
|
||||
logging.error('Invalid out directory: %s.', out_dir)
|
||||
if not os.path.exists(workspace.out):
|
||||
logging.error('Invalid out directory: %s.', workspace.out)
|
||||
return False
|
||||
if not os.listdir(out_dir):
|
||||
logging.error('No fuzzers found in out directory: %s.', out_dir)
|
||||
if not os.listdir(workspace.out):
|
||||
logging.error('No fuzzers found in out directory: %s.', workspace.out)
|
||||
return False
|
||||
|
||||
docker_args, _ = docker.get_base_docker_run_args(out_dir, sanitizer, language)
|
||||
docker_args, _ = docker.get_base_docker_run_args(workspace, sanitizer,
|
||||
language)
|
||||
if allowed_broken_targets_percentage is not None:
|
||||
docker_args += [
|
||||
'-e',
|
||||
|
@ -216,7 +217,7 @@ def check_fuzzer_build(out_dir,
|
|||
|
||||
def _get_docker_build_fuzzers_args_not_container(host_repo_path):
|
||||
"""Returns arguments to the docker build arguments that are needed to use
|
||||
|host_out_dir| when the host of the OSS-Fuzz builder container is not
|
||||
|host_repo_path| when the host of the OSS-Fuzz builder container is not
|
||||
another container."""
|
||||
return ['-v', f'{host_repo_path}:{host_repo_path}']
|
||||
|
||||
|
|
|
@ -13,11 +13,11 @@
|
|||
# limitations under the License.
|
||||
"""Builds a specific OSS-Fuzz project's fuzzers for CI tools."""
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
|
||||
import build_fuzzers
|
||||
import config_utils
|
||||
import docker
|
||||
|
||||
# pylint: disable=c-extension-no-member
|
||||
# pylint gets confused because of the relative import of cifuzz.
|
||||
|
@ -71,15 +71,13 @@ def main():
|
|||
config.project_name, config.commit_sha, config.pr_ref)
|
||||
return returncode
|
||||
|
||||
out_dir = os.path.join(config.workspace, 'out')
|
||||
|
||||
if not config.bad_build_check:
|
||||
# If we've gotten to this point and we don't need to do bad_build_check,
|
||||
# then the build has succeeded.
|
||||
returncode = 0
|
||||
# yapf: disable
|
||||
elif build_fuzzers.check_fuzzer_build(
|
||||
out_dir,
|
||||
docker.Workspace(config),
|
||||
config.sanitizer,
|
||||
config.language,
|
||||
allowed_broken_targets_percentage=config.allowed_broken_targets_percentage
|
||||
|
|
|
@ -136,7 +136,7 @@ class BuildFuzzersIntegrationTest(unittest.TestCase):
|
|||
def setUp(self):
|
||||
self.tmp_dir_obj = tempfile.TemporaryDirectory()
|
||||
self.workspace = self.tmp_dir_obj.name
|
||||
self.out_dir = os.path.join(self.workspace, 'out')
|
||||
self.out_dir = os.path.join(self.workspace, 'build-out')
|
||||
test_helpers.patch_environ(self)
|
||||
|
||||
def tearDown(self):
|
||||
|
@ -268,29 +268,32 @@ class CheckFuzzerBuildTest(unittest.TestCase):
|
|||
|
||||
def setUp(self):
|
||||
self.tmp_dir_obj = tempfile.TemporaryDirectory()
|
||||
self.test_files_path = os.path.join(self.tmp_dir_obj.name, 'test_files')
|
||||
shutil.copytree(TEST_DATA_PATH, self.test_files_path)
|
||||
workspace_path = os.path.join(self.tmp_dir_obj.name, 'workspace')
|
||||
self.workspace = test_helpers.create_workspace(workspace_path)
|
||||
shutil.copytree(TEST_DATA_PATH, workspace_path)
|
||||
|
||||
def tearDown(self):
|
||||
self.tmp_dir_obj.cleanup()
|
||||
|
||||
def test_correct_fuzzer_build(self):
|
||||
"""Checks check_fuzzer_build function returns True for valid fuzzers."""
|
||||
test_fuzzer_dir = os.path.join(self.test_files_path, 'out')
|
||||
self.assertTrue(
|
||||
build_fuzzers.check_fuzzer_build(test_fuzzer_dir, self.SANITIZER,
|
||||
build_fuzzers.check_fuzzer_build(self.workspace, self.SANITIZER,
|
||||
self.LANGUAGE))
|
||||
|
||||
def test_not_a_valid_fuzz_path(self):
|
||||
"""Tests that False is returned when a bad path is given."""
|
||||
def test_not_a_valid_path(self):
|
||||
"""Tests that False is returned when a nonexistent path is given."""
|
||||
workspace = test_helpers.create_workspace('not/a/valid/path')
|
||||
self.assertFalse(
|
||||
build_fuzzers.check_fuzzer_build('not/a/valid/path', self.SANITIZER,
|
||||
build_fuzzers.check_fuzzer_build(workspace, self.SANITIZER,
|
||||
self.LANGUAGE))
|
||||
|
||||
def test_not_a_valid_fuzzer(self):
|
||||
"""Checks a directory that exists but does not have fuzzers is False."""
|
||||
def test_no_valid_fuzzers(self):
|
||||
"""Tests that False is returned when an empty directory is given."""
|
||||
with tempfile.TemporaryDirectory() as tmp_dir:
|
||||
workspace = test_helpers.create_workspace(tmp_dir)
|
||||
self.assertFalse(
|
||||
build_fuzzers.check_fuzzer_build(self.test_files_path, self.SANITIZER,
|
||||
build_fuzzers.check_fuzzer_build(workspace, self.SANITIZER,
|
||||
self.LANGUAGE))
|
||||
|
||||
@mock.patch('helper.docker_run')
|
||||
|
@ -298,8 +301,7 @@ class CheckFuzzerBuildTest(unittest.TestCase):
|
|||
"""Tests that ALLOWED_BROKEN_TARGETS_PERCENTAGE is set when running
|
||||
docker if passed to check_fuzzer_build."""
|
||||
mocked_docker_run.return_value = 0
|
||||
test_fuzzer_dir = os.path.join(TEST_DATA_PATH, 'out')
|
||||
build_fuzzers.check_fuzzer_build(test_fuzzer_dir,
|
||||
build_fuzzers.check_fuzzer_build(self.workspace,
|
||||
self.SANITIZER,
|
||||
self.LANGUAGE,
|
||||
allowed_broken_targets_percentage='0')
|
||||
|
|
|
@ -30,13 +30,11 @@ import utils
|
|||
class BaseClusterFuzzDeployment:
|
||||
"""Base class for ClusterFuzz deployments."""
|
||||
|
||||
CORPUS_DIR_NAME = 'cifuzz-corpus'
|
||||
BUILD_DIR_NAME = 'cifuzz-latest-build'
|
||||
|
||||
def __init__(self, config):
|
||||
def __init__(self, config, workspace):
|
||||
self.config = config
|
||||
self.workspace = workspace
|
||||
|
||||
def download_latest_build(self, parent_dir):
|
||||
def download_latest_build(self):
|
||||
"""Downloads the latest build from ClusterFuzz.
|
||||
|
||||
Returns:
|
||||
|
@ -44,47 +42,37 @@ class BaseClusterFuzzDeployment:
|
|||
"""
|
||||
raise NotImplementedError('Child class must implement method.')
|
||||
|
||||
def upload_latest_build(self, build_dir):
|
||||
def upload_latest_build(self):
|
||||
"""Uploads the latest build to the filestore.
|
||||
Returns:
|
||||
True on success.
|
||||
"""
|
||||
raise NotImplementedError('Child class must implement method.')
|
||||
|
||||
def download_corpus(self, target_name, parent_dir):
|
||||
"""Downloads the corpus for |target_name| from ClusterFuzz to a subdirectory
|
||||
of |parent_dir|.
|
||||
def download_corpus(self, target_name):
|
||||
"""Downloads the corpus for |target_name| from ClusterFuzz.
|
||||
|
||||
Returns:
|
||||
A path to where the OSS-Fuzz build was stored, or None if it wasn't.
|
||||
"""
|
||||
raise NotImplementedError('Child class must implement method.')
|
||||
|
||||
def upload_crashes(self, crashes_dir):
|
||||
def upload_crashes(self):
|
||||
"""Uploads crashes in |crashes_dir| to filestore."""
|
||||
raise NotImplementedError('Child class must implement method.')
|
||||
|
||||
def get_target_corpus_dir(self, target_name, parent_dir):
|
||||
"""Returns the path to the corpus dir for |target_name| within
|
||||
|parent_dir|."""
|
||||
return os.path.join(self.get_corpus_dir(parent_dir), target_name)
|
||||
def get_target_corpus_dir(self, target_name):
|
||||
"""Returns the path to the corpus dir for |target_name|."""
|
||||
return os.path.join(self.workspace.corpora, target_name)
|
||||
|
||||
def get_corpus_dir(self, parent_dir):
|
||||
"""Returns the path to the corpus dir within |parent_dir|."""
|
||||
return os.path.join(parent_dir, self.CORPUS_DIR_NAME)
|
||||
|
||||
def get_build_dir(self, parent_dir):
|
||||
"""Returns the path to the build dir for within |parent_dir|."""
|
||||
return os.path.join(parent_dir, self.BUILD_DIR_NAME)
|
||||
|
||||
def upload_corpus(self, target_name, corpus_dir): # pylint: disable=no-self-use,unused-argument
|
||||
"""Uploads the corpus for |target_name| in |corpus_dir| to filestore."""
|
||||
def upload_corpus(self, target_name): # pylint: disable=no-self-use,unused-argument
|
||||
"""Uploads the corpus for |target_name| to filestore."""
|
||||
raise NotImplementedError('Child class must implement method.')
|
||||
|
||||
def make_empty_corpus_dir(self, target_name, parent_dir):
|
||||
def make_empty_corpus_dir(self, target_name):
|
||||
"""Makes an empty corpus directory for |target_name| in |parent_dir| and
|
||||
returns the path to the directory."""
|
||||
corpus_dir = self.get_target_corpus_dir(target_name, parent_dir)
|
||||
corpus_dir = self.get_target_corpus_dir(target_name)
|
||||
os.makedirs(corpus_dir, exist_ok=True)
|
||||
return corpus_dir
|
||||
|
||||
|
@ -94,35 +82,35 @@ class ClusterFuzzLite(BaseClusterFuzzDeployment):
|
|||
|
||||
BASE_BUILD_NAME = 'cifuzz-build-'
|
||||
|
||||
def __init__(self, config):
|
||||
super().__init__(config)
|
||||
def __init__(self, config, workspace):
|
||||
super().__init__(config, workspace)
|
||||
self.filestore = filestore_utils.get_filestore(self.config)
|
||||
|
||||
def download_latest_build(self, parent_dir):
|
||||
build_dir = self.get_build_dir(parent_dir)
|
||||
if os.path.exists(build_dir):
|
||||
def download_latest_build(self):
|
||||
if os.path.exists(self.workspace.clusterfuzz_build):
|
||||
# This path is necessary because download_latest_build can be called
|
||||
# multiple times.That is the case because it is called only when we need
|
||||
# to see if a bug is novel, i.e. until we want to check a bug is novel we
|
||||
# don't want to waste time calling this, but therefore this method can be
|
||||
# called if multiple bugs are found.
|
||||
return build_dir
|
||||
return self.workspace.clusterfuzz_build
|
||||
|
||||
os.makedirs(build_dir, exist_ok=True)
|
||||
os.makedirs(self.workspace.clusterfuzz_build, exist_ok=True)
|
||||
build_name = self._get_build_name()
|
||||
|
||||
try:
|
||||
if self.filestore.download_latest_build(build_name, build_dir):
|
||||
return build_dir
|
||||
if self.filestore.download_latest_build(build_name,
|
||||
self.workspace.clusterfuzz_build):
|
||||
return self.workspace.clusterfuzz_build
|
||||
except Exception as err: # pylint: disable=broad-except
|
||||
logging.error('Could not download latest build because of: %s.', err)
|
||||
|
||||
return None
|
||||
|
||||
def download_corpus(self, target_name, parent_dir):
|
||||
corpus_dir = self.make_empty_corpus_dir(target_name, parent_dir)
|
||||
def download_corpus(self, target_name):
|
||||
corpus_dir = self.make_empty_corpus_dir(target_name)
|
||||
logging.debug('ClusterFuzzLite: downloading corpus for %s to %s.',
|
||||
target_name, parent_dir)
|
||||
target_name, corpus_dir)
|
||||
corpus_name = self._get_corpus_name(target_name)
|
||||
try:
|
||||
self.filestore.download_corpus(corpus_name, corpus_dir)
|
||||
|
@ -142,8 +130,9 @@ class ClusterFuzzLite(BaseClusterFuzzDeployment):
|
|||
"""Returns the name of the crashes artifact."""
|
||||
return 'crashes'
|
||||
|
||||
def upload_corpus(self, target_name, corpus_dir):
|
||||
def upload_corpus(self, target_name):
|
||||
"""Upload the corpus produced by |target_name| in |corpus_dir|."""
|
||||
corpus_dir = self.get_target_corpus_dir(target_name)
|
||||
logging.info('Uploading corpus in %s for %s.', corpus_dir, target_name)
|
||||
name = self._get_corpus_name(target_name)
|
||||
try:
|
||||
|
@ -152,25 +141,28 @@ class ClusterFuzzLite(BaseClusterFuzzDeployment):
|
|||
logging.error('Failed to upload corpus for target: %s. Error: %s.',
|
||||
target_name, error)
|
||||
|
||||
def upload_latest_build(self, build_dir):
|
||||
logging.info('Uploading latest build in %s.', build_dir)
|
||||
def upload_latest_build(self):
|
||||
logging.info('Uploading latest build in %s.',
|
||||
self.workspace.clusterfuzz_build)
|
||||
build_name = self._get_build_name()
|
||||
try:
|
||||
return self.filestore.upload_directory(build_name, build_dir)
|
||||
return self.filestore.upload_directory(build_name,
|
||||
self.workspace.clusterfuzz_build)
|
||||
except Exception as error: # pylint: disable=broad-except
|
||||
logging.error('Failed to upload latest build: %s. Error: %s.', build_dir,
|
||||
error)
|
||||
logging.error('Failed to upload latest build: %s. Error: %s.',
|
||||
self.workspace.clusterfuzz_build, error)
|
||||
|
||||
def upload_crashes(self, crashes_dir):
|
||||
if not os.listdir(crashes_dir):
|
||||
logging.info('No crashes in %s. Not uploading.', crashes_dir)
|
||||
def upload_crashes(self):
|
||||
if not os.listdir(self.workspace.artifacts):
|
||||
logging.info('No crashes in %s. Not uploading.', self.workspace.artifacts)
|
||||
return
|
||||
|
||||
crashes_artifact_name = self._get_crashes_artifact_name()
|
||||
|
||||
logging.info('Uploading crashes in %s', crashes_dir)
|
||||
logging.info('Uploading crashes in %s', self.workspace.artifacts)
|
||||
try:
|
||||
self.filestore.upload_directory(crashes_artifact_name, crashes_dir)
|
||||
self.filestore.upload_directory(crashes_artifact_name,
|
||||
self.workspace.artifacts)
|
||||
except Exception as error: # pylint: disable=broad-except
|
||||
logging.error('Failed to upload crashes. Error: %s.', error)
|
||||
|
||||
|
@ -202,19 +194,18 @@ class OSSFuzz(BaseClusterFuzzDeployment):
|
|||
return None
|
||||
return response.read().decode()
|
||||
|
||||
def download_latest_build(self, parent_dir):
|
||||
def download_latest_build(self):
|
||||
"""Downloads the latest OSS-Fuzz build from GCS.
|
||||
|
||||
Returns:
|
||||
A path to where the OSS-Fuzz build was stored, or None if it wasn't.
|
||||
"""
|
||||
build_dir = self.get_build_dir(parent_dir)
|
||||
if os.path.exists(build_dir):
|
||||
if os.path.exists(self.workspace.clusterfuzz_build):
|
||||
# This function can be called multiple times, don't download the build
|
||||
# again.
|
||||
return build_dir
|
||||
return self.workspace.clusterfuzz_build
|
||||
|
||||
os.makedirs(build_dir, exist_ok=True)
|
||||
os.makedirs(self.workspace.clusterfuzz_build, exist_ok=True)
|
||||
|
||||
latest_build_name = self.get_latest_build_name()
|
||||
if not latest_build_name:
|
||||
|
@ -224,30 +215,31 @@ class OSSFuzz(BaseClusterFuzzDeployment):
|
|||
self.CLUSTERFUZZ_BUILDS,
|
||||
self.config.project_name,
|
||||
latest_build_name)
|
||||
if http_utils.download_and_unpack_zip(oss_fuzz_build_url, build_dir):
|
||||
return build_dir
|
||||
if http_utils.download_and_unpack_zip(oss_fuzz_build_url,
|
||||
self.workspace.clusterfuzz_build):
|
||||
return self.workspace.clusterfuzz_build
|
||||
|
||||
return None
|
||||
|
||||
def upload_latest_build(self, build_dir): # pylint: disable=no-self-use,unused-argument
|
||||
def upload_latest_build(self): # pylint: disable=no-self-use
|
||||
"""Noop Impelementation of upload_latest_build."""
|
||||
logging.info('Not uploading latest build because on OSS-Fuzz.')
|
||||
|
||||
def upload_corpus(self, target_name, corpus_dir): # pylint: disable=no-self-use,unused-argument
|
||||
def upload_corpus(self, target_name): # pylint: disable=no-self-use,unused-argument
|
||||
"""Noop Impelementation of upload_corpus."""
|
||||
logging.info('Not uploading corpus because on OSS-Fuzz.')
|
||||
|
||||
def upload_crashes(self, crashes_dir): # pylint: disable=no-self-use,unused-argument
|
||||
def upload_crashes(self): # pylint: disable=no-self-use
|
||||
"""Noop Impelementation of upload_crashes."""
|
||||
logging.info('Not uploading crashes because on OSS-Fuzz.')
|
||||
|
||||
def download_corpus(self, target_name, parent_dir):
|
||||
def download_corpus(self, target_name):
|
||||
"""Downloads the latest OSS-Fuzz corpus for the target.
|
||||
|
||||
Returns:
|
||||
The local path to to corpus or None if download failed.
|
||||
"""
|
||||
corpus_dir = self.make_empty_corpus_dir(target_name, parent_dir)
|
||||
corpus_dir = self.make_empty_corpus_dir(target_name)
|
||||
project_qualified_fuzz_target_name = target_name
|
||||
qualified_name_prefix = self.config.project_name + '_'
|
||||
if not target_name.startswith(qualified_name_prefix):
|
||||
|
@ -267,38 +259,38 @@ class NoClusterFuzzDeployment(BaseClusterFuzzDeployment):
|
|||
"""ClusterFuzzDeployment implementation used when there is no deployment of
|
||||
ClusterFuzz to use."""
|
||||
|
||||
def upload_latest_build(self, build_dir): # pylint: disable=no-self-use,unused-argument
|
||||
def upload_latest_build(self): # pylint: disable=no-self-use
|
||||
"""Noop Impelementation of upload_latest_build."""
|
||||
logging.info('Not uploading latest build because no ClusterFuzz '
|
||||
'deployment.')
|
||||
|
||||
def upload_corpus(self, target_name, corpus_dir): # pylint: disable=no-self-use,unused-argument
|
||||
def upload_corpus(self, target_name): # pylint: disable=no-self-use,unused-argument
|
||||
"""Noop Impelementation of upload_corpus."""
|
||||
logging.info('Not uploading corpus because no ClusterFuzz deployment.')
|
||||
|
||||
def upload_crashes(self, crashes_dir): # pylint: disable=no-self-use,unused-argument
|
||||
def upload_crashes(self): # pylint: disable=no-self-use
|
||||
"""Noop Impelementation of upload_crashes."""
|
||||
logging.info('Not uploading crashes because no ClusterFuzz deployment.')
|
||||
|
||||
def download_corpus(self, target_name, parent_dir): # pylint: disable=no-self-use,unused-argument
|
||||
def download_corpus(self, target_name): # pylint: disable=no-self-use,unused-argument
|
||||
"""Noop Impelementation of download_corpus."""
|
||||
logging.info('Not downloading corpus because no ClusterFuzz deployment.')
|
||||
return self.make_empty_corpus_dir(target_name, parent_dir)
|
||||
return self.make_empty_corpus_dir(target_name)
|
||||
|
||||
def download_latest_build(self, parent_dir): # pylint: disable=no-self-use,unused-argument
|
||||
def download_latest_build(self): # pylint: disable=no-self-use
|
||||
"""Noop Impelementation of download_latest_build."""
|
||||
logging.info(
|
||||
'Not downloading latest build because no ClusterFuzz deployment.')
|
||||
|
||||
|
||||
def get_clusterfuzz_deployment(config):
|
||||
def get_clusterfuzz_deployment(config, workspace):
|
||||
"""Returns object reprsenting deployment of ClusterFuzz used by |config|."""
|
||||
if (config.platform == config.Platform.INTERNAL_GENERIC_CI or
|
||||
config.platform == config.Platform.INTERNAL_GITHUB):
|
||||
logging.info('Using OSS-Fuzz as ClusterFuzz deployment.')
|
||||
return OSSFuzz(config)
|
||||
return OSSFuzz(config, workspace)
|
||||
if config.platform == config.Platform.EXTERNAL_GENERIC_CI:
|
||||
logging.info('Not using a ClusterFuzz deployment.')
|
||||
return NoClusterFuzzDeployment(config)
|
||||
return NoClusterFuzzDeployment(config, workspace)
|
||||
logging.info('Using ClusterFuzzLite as ClusterFuzz deployment.')
|
||||
return ClusterFuzzLite(config)
|
||||
return ClusterFuzzLite(config, workspace)
|
||||
|
|
|
@ -22,6 +22,7 @@ from pyfakefs import fake_filesystem_unittest
|
|||
|
||||
import clusterfuzz_deployment
|
||||
import config_utils
|
||||
import docker
|
||||
import test_helpers
|
||||
|
||||
# NOTE: This integration test relies on
|
||||
|
@ -31,15 +32,19 @@ EXAMPLE_PROJECT = 'example'
|
|||
# An example fuzzer that triggers an error.
|
||||
EXAMPLE_FUZZER = 'example_crash_fuzzer'
|
||||
|
||||
OUT_DIR = '/out'
|
||||
EXPECTED_LATEST_BUILD_PATH = os.path.join(OUT_DIR, 'cifuzz-latest-build')
|
||||
WORKSPACE = '/workspace'
|
||||
EXPECTED_LATEST_BUILD_PATH = os.path.join(WORKSPACE, 'cifuzz-prev-build')
|
||||
|
||||
|
||||
def _create_config(**kwargs):
|
||||
"""Creates a config object and then sets every attribute that is a key in
|
||||
|kwargs| to the corresponding value. Asserts that each key in |kwargs| is an
|
||||
attribute of Config."""
|
||||
defaults = {'is_github': True, 'project_name': EXAMPLE_PROJECT}
|
||||
defaults = {
|
||||
'is_github': True,
|
||||
'project_name': EXAMPLE_PROJECT,
|
||||
'workspace': WORKSPACE,
|
||||
}
|
||||
for default_key, default_value in defaults.items():
|
||||
if default_key not in kwargs:
|
||||
kwargs[default_key] = default_value
|
||||
|
@ -49,7 +54,8 @@ def _create_config(**kwargs):
|
|||
|
||||
def _create_deployment(**kwargs):
|
||||
config = _create_config(**kwargs)
|
||||
return clusterfuzz_deployment.get_clusterfuzz_deployment(config)
|
||||
workspace = docker.Workspace(config)
|
||||
return clusterfuzz_deployment.get_clusterfuzz_deployment(config, workspace)
|
||||
|
||||
|
||||
class OSSFuzzTest(fake_filesystem_unittest.TestCase):
|
||||
|
@ -61,10 +67,11 @@ class OSSFuzzTest(fake_filesystem_unittest.TestCase):
|
|||
|
||||
@mock.patch('http_utils.download_and_unpack_zip', return_value=True)
|
||||
def test_download_corpus(self, mocked_download_and_unpack_zip):
|
||||
"""Tests that download_corpus works for a valid project."""
|
||||
result = self.deployment.download_corpus(EXAMPLE_FUZZER, OUT_DIR)
|
||||
"""Tests that we can download a corpus for a valid project."""
|
||||
result = self.deployment.download_corpus(EXAMPLE_FUZZER)
|
||||
self.assertIsNotNone(result)
|
||||
expected_corpus_dir = os.path.join(OUT_DIR, 'cifuzz-corpus', EXAMPLE_FUZZER)
|
||||
expected_corpus_dir = os.path.join(self.deployment.workspace.corpora,
|
||||
EXAMPLE_FUZZER)
|
||||
expected_url = ('https://storage.googleapis.com/example-backup.'
|
||||
'clusterfuzz-external.appspot.com/corpus/libFuzzer/'
|
||||
'example_crash_fuzzer/public.zip')
|
||||
|
@ -75,8 +82,9 @@ class OSSFuzzTest(fake_filesystem_unittest.TestCase):
|
|||
def test_download_corpus_fail(self, _):
|
||||
"""Tests that when downloading fails, an empty corpus directory is still
|
||||
returned."""
|
||||
corpus_path = self.deployment.download_corpus(EXAMPLE_FUZZER, OUT_DIR)
|
||||
self.assertEqual(corpus_path, '/out/cifuzz-corpus/example_crash_fuzzer')
|
||||
corpus_path = self.deployment.download_corpus(EXAMPLE_FUZZER)
|
||||
self.assertEqual(corpus_path,
|
||||
'/workspace/cifuzz-corpus/example_crash_fuzzer')
|
||||
self.assertEqual(os.listdir(corpus_path), [])
|
||||
|
||||
def test_get_latest_build_name(self):
|
||||
|
@ -86,12 +94,11 @@ class OSSFuzzTest(fake_filesystem_unittest.TestCase):
|
|||
self.assertTrue('address' in latest_build_name)
|
||||
|
||||
@parameterized.parameterized.expand([
|
||||
('upload_latest_build', ('build',),
|
||||
('upload_latest_build', tuple(),
|
||||
'Not uploading latest build because on OSS-Fuzz.'),
|
||||
('upload_corpus', ('target', 'corpus_dir'),
|
||||
('upload_corpus', ('target',),
|
||||
'Not uploading corpus because on OSS-Fuzz.'),
|
||||
('upload_crashes', ('crashes_dir',),
|
||||
'Not uploading crashes because on OSS-Fuzz.'),
|
||||
('upload_crashes', tuple(), 'Not uploading crashes because on OSS-Fuzz.'),
|
||||
])
|
||||
def test_noop_methods(self, method, method_args, expected_message):
|
||||
"""Tests that certain methods are noops for OSS-Fuzz."""
|
||||
|
@ -104,7 +111,7 @@ class OSSFuzzTest(fake_filesystem_unittest.TestCase):
|
|||
def test_download_latest_build(self, mocked_download_and_unpack_zip):
|
||||
"""Tests that downloading the latest build works as intended under normal
|
||||
circumstances."""
|
||||
self.assertEqual(self.deployment.download_latest_build(OUT_DIR),
|
||||
self.assertEqual(self.deployment.download_latest_build(),
|
||||
EXPECTED_LATEST_BUILD_PATH)
|
||||
expected_url = ('https://storage.googleapis.com/clusterfuzz-builds/example/'
|
||||
'example-address-202008030600.zip')
|
||||
|
@ -115,7 +122,7 @@ class OSSFuzzTest(fake_filesystem_unittest.TestCase):
|
|||
def test_download_latest_build_fail(self, _):
|
||||
"""Tests that download_latest_build returns None when it fails to download a
|
||||
build."""
|
||||
self.assertIsNone(self.deployment.download_latest_build(OUT_DIR))
|
||||
self.assertIsNone(self.deployment.download_latest_build())
|
||||
|
||||
|
||||
class ClusterFuzzLiteTest(fake_filesystem_unittest.TestCase):
|
||||
|
@ -130,8 +137,9 @@ class ClusterFuzzLiteTest(fake_filesystem_unittest.TestCase):
|
|||
return_value=True)
|
||||
def test_download_corpus(self, mocked_download_corpus):
|
||||
"""Tests that download_corpus works for a valid project."""
|
||||
result = self.deployment.download_corpus(EXAMPLE_FUZZER, OUT_DIR)
|
||||
expected_corpus_dir = os.path.join(OUT_DIR, 'cifuzz-corpus', EXAMPLE_FUZZER)
|
||||
result = self.deployment.download_corpus(EXAMPLE_FUZZER)
|
||||
expected_corpus_dir = os.path.join(WORKSPACE, 'cifuzz-corpus',
|
||||
EXAMPLE_FUZZER)
|
||||
self.assertEqual(result, expected_corpus_dir)
|
||||
mocked_download_corpus.assert_called_with('corpus-example_crash_fuzzer',
|
||||
expected_corpus_dir)
|
||||
|
@ -141,8 +149,9 @@ class ClusterFuzzLiteTest(fake_filesystem_unittest.TestCase):
|
|||
def test_download_corpus_fail(self, _):
|
||||
"""Tests that when downloading fails, an empty corpus directory is still
|
||||
returned."""
|
||||
corpus_path = self.deployment.download_corpus(EXAMPLE_FUZZER, OUT_DIR)
|
||||
self.assertEqual(corpus_path, '/out/cifuzz-corpus/example_crash_fuzzer')
|
||||
corpus_path = self.deployment.download_corpus(EXAMPLE_FUZZER)
|
||||
self.assertEqual(corpus_path,
|
||||
'/workspace/cifuzz-corpus/example_crash_fuzzer')
|
||||
self.assertEqual(os.listdir(corpus_path), [])
|
||||
|
||||
@mock.patch(
|
||||
|
@ -151,7 +160,7 @@ class ClusterFuzzLiteTest(fake_filesystem_unittest.TestCase):
|
|||
def test_download_latest_build(self, mocked_download_latest_build):
|
||||
"""Tests that downloading the latest build works as intended under normal
|
||||
circumstances."""
|
||||
self.assertEqual(self.deployment.download_latest_build(OUT_DIR),
|
||||
self.assertEqual(self.deployment.download_latest_build(),
|
||||
EXPECTED_LATEST_BUILD_PATH)
|
||||
expected_artifact_name = 'cifuzz-build-address'
|
||||
mocked_download_latest_build.assert_called_with(expected_artifact_name,
|
||||
|
@ -163,7 +172,7 @@ class ClusterFuzzLiteTest(fake_filesystem_unittest.TestCase):
|
|||
def test_download_latest_build_fail(self, _):
|
||||
"""Tests that download_latest_build returns None when it fails to download a
|
||||
build."""
|
||||
self.assertIsNone(self.deployment.download_latest_build(OUT_DIR))
|
||||
self.assertIsNone(self.deployment.download_latest_build())
|
||||
|
||||
|
||||
class NoClusterFuzzDeploymentTest(fake_filesystem_unittest.TestCase):
|
||||
|
@ -173,26 +182,30 @@ class NoClusterFuzzDeploymentTest(fake_filesystem_unittest.TestCase):
|
|||
self.setUpPyfakefs()
|
||||
config = test_helpers.create_run_config(project_name=EXAMPLE_PROJECT,
|
||||
build_integration_path='/',
|
||||
workspace=WORKSPACE,
|
||||
is_github=False)
|
||||
self.deployment = clusterfuzz_deployment.get_clusterfuzz_deployment(config)
|
||||
workspace = docker.Workspace(config)
|
||||
self.deployment = clusterfuzz_deployment.get_clusterfuzz_deployment(
|
||||
config, workspace)
|
||||
|
||||
@mock.patch('logging.info')
|
||||
def test_download_corpus(self, mocked_info):
|
||||
"""Tests that download corpus returns the path to the empty corpus
|
||||
directory."""
|
||||
corpus_path = self.deployment.download_corpus(EXAMPLE_FUZZER, OUT_DIR)
|
||||
self.assertEqual(corpus_path, '/out/cifuzz-corpus/example_crash_fuzzer')
|
||||
corpus_path = self.deployment.download_corpus(EXAMPLE_FUZZER)
|
||||
self.assertEqual(corpus_path,
|
||||
'/workspace/cifuzz-corpus/example_crash_fuzzer')
|
||||
mocked_info.assert_called_with(
|
||||
'Not downloading corpus because no ClusterFuzz deployment.')
|
||||
|
||||
@parameterized.parameterized.expand([
|
||||
('upload_latest_build', ('build',),
|
||||
('upload_latest_build', tuple(),
|
||||
'Not uploading latest build because no ClusterFuzz deployment.'),
|
||||
('upload_corpus', ('target', 'corpus_dir'),
|
||||
('upload_corpus', ('target',),
|
||||
'Not uploading corpus because no ClusterFuzz deployment.'),
|
||||
('upload_crashes', ('crashes_dir',),
|
||||
('upload_crashes', tuple(),
|
||||
'Not uploading crashes because no ClusterFuzz deployment.'),
|
||||
('download_latest_build', ('parent_dir',),
|
||||
('download_latest_build', tuple(),
|
||||
'Not downloading latest build because no ClusterFuzz deployment.')
|
||||
])
|
||||
def test_noop_methods(self, method, method_args, expected_message):
|
||||
|
@ -222,10 +235,12 @@ class GetClusterFuzzDeploymentTest(unittest.TestCase):
|
|||
return_value=platform,
|
||||
new_callable=mock.PropertyMock):
|
||||
with mock.patch('filestore_utils.get_filestore', return_value=None):
|
||||
config = test_helpers.create_run_config()
|
||||
config = _create_config()
|
||||
workspace = docker.Workspace(config)
|
||||
|
||||
self.assertIsInstance(
|
||||
clusterfuzz_deployment.get_clusterfuzz_deployment(config),
|
||||
expected_deployment_cls)
|
||||
clusterfuzz_deployment.get_clusterfuzz_deployment(
|
||||
config, workspace), expected_deployment_cls)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
|
|
@ -53,36 +53,77 @@ def delete_images(images):
|
|||
utils.execute(['docker', 'builder', 'prune', '-f'])
|
||||
|
||||
|
||||
def get_base_docker_run_args(out_dir, sanitizer='address', language='c++'):
|
||||
def get_base_docker_run_args(workspace, sanitizer='address', language='c++'):
|
||||
"""Returns arguments that should be passed to every invocation of 'docker
|
||||
run'."""
|
||||
docker_args = _DEFAULT_DOCKER_RUN_ARGS.copy()
|
||||
docker_args += [
|
||||
'-e',
|
||||
f'SANITIZER={sanitizer}',
|
||||
'-e',
|
||||
f'FUZZING_LANGUAGE={language}',
|
||||
'-e', f'SANITIZER={sanitizer}', '-e', f'FUZZING_LANGUAGE={language}',
|
||||
'-e', 'OUT=' + workspace.out
|
||||
]
|
||||
docker_container = utils.get_container_name()
|
||||
if docker_container:
|
||||
docker_args += ['--volumes-from', docker_container, '-e', 'OUT=' + out_dir]
|
||||
# Don't map specific volumes if in a docker container, it breaks when
|
||||
# running a sibling container.
|
||||
docker_args += ['--volumes-from', docker_container]
|
||||
else:
|
||||
docker_args += get_args_mapping_host_path_to_container(out_dir, '/out')
|
||||
docker_args += _get_args_mapping_host_path_to_container(workspace.workspace)
|
||||
return docker_args, docker_container
|
||||
|
||||
|
||||
def get_base_docker_run_command(out_dir, sanitizer='address', language='c++'):
|
||||
def get_base_docker_run_command(workspace, sanitizer='address', language='c++'):
|
||||
"""Returns part of the command that should be used everytime 'docker run' is
|
||||
invoked."""
|
||||
docker_args, docker_container = get_base_docker_run_args(
|
||||
out_dir, sanitizer, language)
|
||||
workspace, sanitizer, language)
|
||||
command = _DEFAULT_DOCKER_RUN_COMMAND.copy() + docker_args
|
||||
return command, docker_container
|
||||
|
||||
|
||||
def get_args_mapping_host_path_to_container(host_path, container_path=None):
|
||||
def _get_args_mapping_host_path_to_container(host_path, container_path=None):
|
||||
"""Get arguments to docker run that will map |host_path| a path on the host to
|
||||
a path in the container. If |container_path| is specified, that path is mapped
|
||||
to. If not, then |host_path| is mapped to itself in the container."""
|
||||
# WARNING: Do not use this function when running in production (and
|
||||
# --volumes-from) is used for mapping volumes. It will break production.
|
||||
container_path = host_path if container_path is None else container_path
|
||||
return ['-v', f'{host_path}:{container_path}']
|
||||
|
||||
|
||||
class Workspace:
|
||||
"""Class representing the workspace directory."""
|
||||
|
||||
def __init__(self, config):
|
||||
self.workspace = config.workspace
|
||||
|
||||
def initialize_dir(self, directory): # pylint: disable=no-self-use
|
||||
"""Creates directory if it doesn't already exist, otherwise does nothing."""
|
||||
os.makedirs(directory, exist_ok=True)
|
||||
|
||||
@property
|
||||
def out(self):
|
||||
"""The out directory used for storing the fuzzer build built by
|
||||
build_fuzzers."""
|
||||
# Don't use 'out' because it needs to be used by artifacts.
|
||||
return os.path.join(self.workspace, 'build-out')
|
||||
|
||||
@property
|
||||
def work(self):
|
||||
"""The directory used as the work directory for the fuzzer build/run."""
|
||||
return os.path.join(self.workspace, 'work')
|
||||
|
||||
@property
|
||||
def artifacts(self):
|
||||
"""The directory used to store artifacts for download by CI-system users."""
|
||||
# This is hardcoded by a lot of clients, so we need to use this.
|
||||
return os.path.join(self.workspace, 'out', 'artifacts')
|
||||
|
||||
@property
|
||||
def clusterfuzz_build(self):
|
||||
"""The directory where builds from ClusterFuzz are stored."""
|
||||
return os.path.join(self.workspace, 'cifuzz-prev-build')
|
||||
|
||||
@property
|
||||
def corpora(self):
|
||||
"""The directory where corpora from ClusterFuzz are stored."""
|
||||
return os.path.join(self.workspace, 'cifuzz-corpus')
|
||||
|
|
|
@ -15,10 +15,13 @@
|
|||
import unittest
|
||||
from unittest import mock
|
||||
|
||||
import config_utils
|
||||
import docker
|
||||
|
||||
CONTAINER_NAME = 'example-container'
|
||||
OUT_DIR = '/example-out'
|
||||
config = config_utils.RunFuzzersConfig()
|
||||
config.workspace = '/workspace'
|
||||
WORKSPACE = docker.Workspace(config)
|
||||
SANITIZER = 'example-sanitizer'
|
||||
LANGUAGE = 'example-language'
|
||||
|
||||
|
@ -57,14 +60,26 @@ class GetBaseDockerRunArgsTest(unittest.TestCase):
|
|||
"""Tests that get_base_docker_run_args works as intended when inside a
|
||||
container."""
|
||||
docker_args, docker_container = docker.get_base_docker_run_args(
|
||||
OUT_DIR, SANITIZER, LANGUAGE)
|
||||
WORKSPACE, SANITIZER, LANGUAGE)
|
||||
self.assertEqual(docker_container, CONTAINER_NAME)
|
||||
expected_docker_args = []
|
||||
expected_docker_args = [
|
||||
'--cap-add', 'SYS_PTRACE', '-e', 'FUZZING_ENGINE=libfuzzer', '-e',
|
||||
'ARCHITECTURE=x86_64', '-e', 'CIFUZZ=True', '-e',
|
||||
f'SANITIZER={SANITIZER}', '-e', f'FUZZING_LANGUAGE={LANGUAGE}',
|
||||
'--volumes-from', CONTAINER_NAME, '-e', f'OUT={OUT_DIR}'
|
||||
'--cap-add',
|
||||
'SYS_PTRACE',
|
||||
'-e',
|
||||
'FUZZING_ENGINE=libfuzzer',
|
||||
'-e',
|
||||
'ARCHITECTURE=x86_64',
|
||||
'-e',
|
||||
'CIFUZZ=True',
|
||||
'-e',
|
||||
f'SANITIZER={SANITIZER}',
|
||||
'-e',
|
||||
f'FUZZING_LANGUAGE={LANGUAGE}',
|
||||
'-e',
|
||||
f'OUT={WORKSPACE.out}',
|
||||
'--volumes-from',
|
||||
CONTAINER_NAME,
|
||||
]
|
||||
self.assertEqual(docker_args, expected_docker_args)
|
||||
|
||||
|
@ -73,13 +88,14 @@ class GetBaseDockerRunArgsTest(unittest.TestCase):
|
|||
"""Tests that get_base_docker_run_args works as intended when not inside a
|
||||
container."""
|
||||
docker_args, docker_container = docker.get_base_docker_run_args(
|
||||
OUT_DIR, SANITIZER, LANGUAGE)
|
||||
WORKSPACE, SANITIZER, LANGUAGE)
|
||||
self.assertEqual(docker_container, None)
|
||||
expected_docker_args = [
|
||||
'--cap-add', 'SYS_PTRACE', '-e', 'FUZZING_ENGINE=libfuzzer', '-e',
|
||||
'ARCHITECTURE=x86_64', '-e', 'CIFUZZ=True', '-e',
|
||||
f'SANITIZER={SANITIZER}', '-e', f'FUZZING_LANGUAGE={LANGUAGE}', '-v',
|
||||
f'{OUT_DIR}:/out'
|
||||
f'SANITIZER={SANITIZER}', '-e', f'FUZZING_LANGUAGE={LANGUAGE}', '-e',
|
||||
f'OUT={WORKSPACE.out}', '-v',
|
||||
f'{WORKSPACE.workspace}:{WORKSPACE.workspace}'
|
||||
]
|
||||
self.assertEqual(docker_args, expected_docker_args)
|
||||
|
||||
|
@ -92,12 +108,13 @@ class GetBaseDockerRunCommandTest(unittest.TestCase):
|
|||
"""Tests that get_base_docker_run_args works as intended when not inside a
|
||||
container."""
|
||||
docker_args, docker_container = docker.get_base_docker_run_command(
|
||||
OUT_DIR, SANITIZER, LANGUAGE)
|
||||
WORKSPACE, SANITIZER, LANGUAGE)
|
||||
self.assertEqual(docker_container, None)
|
||||
expected_docker_command = [
|
||||
'docker', 'run', '--rm', '--privileged', '--cap-add', 'SYS_PTRACE',
|
||||
'-e', 'FUZZING_ENGINE=libfuzzer', '-e', 'ARCHITECTURE=x86_64', '-e',
|
||||
'CIFUZZ=True', '-e', f'SANITIZER={SANITIZER}', '-e',
|
||||
f'FUZZING_LANGUAGE={LANGUAGE}', '-v', f'{OUT_DIR}:/out'
|
||||
f'FUZZING_LANGUAGE={LANGUAGE}', '-e', f'OUT={WORKSPACE.out}', '-v',
|
||||
f'{WORKSPACE.workspace}:{WORKSPACE.workspace}'
|
||||
]
|
||||
self.assertEqual(docker_args, expected_docker_command)
|
||||
|
|
|
@ -33,7 +33,7 @@ logging.basicConfig(
|
|||
|
||||
# Use a fixed seed for determinism. Use len_control=0 since we don't have enough
|
||||
# time fuzzing for len_control to make sense (probably).
|
||||
LIBFUZZER_OPTIONS = '-seed=1337 -len_control=0'
|
||||
LIBFUZZER_OPTIONS = ['-seed=1337', '-len_control=0']
|
||||
|
||||
# The number of reproduce attempts for a crash.
|
||||
REPRODUCE_ATTEMPTS = 10
|
||||
|
@ -54,25 +54,25 @@ class ReproduceError(Exception):
|
|||
"""Error for when we can't attempt to reproduce a crash."""
|
||||
|
||||
|
||||
class FuzzTarget:
|
||||
class FuzzTarget: # pylint: disable=too-many-instance-attributes
|
||||
"""A class to manage a single fuzz target.
|
||||
|
||||
Attributes:
|
||||
target_name: The name of the fuzz target.
|
||||
duration: The length of time in seconds that the target should run.
|
||||
target_path: The location of the fuzz target binary.
|
||||
out_dir: The location of where output artifacts are stored.
|
||||
workspace: The workspace for storing things related to fuzzing.
|
||||
"""
|
||||
|
||||
# pylint: disable=too-many-arguments
|
||||
def __init__(self, target_path, duration, out_dir, clusterfuzz_deployment,
|
||||
def __init__(self, target_path, duration, workspace, clusterfuzz_deployment,
|
||||
config):
|
||||
"""Represents a single fuzz target.
|
||||
|
||||
Args:
|
||||
target_path: The location of the fuzz target binary.
|
||||
duration: The length of time in seconds the target should run.
|
||||
out_dir: The location of where the output from crashes should be stored.
|
||||
workspace: The path used for storing things needed for fuzzing.
|
||||
clusterfuzz_deployment: The object representing the ClusterFuzz
|
||||
deployment.
|
||||
config: The config of this project.
|
||||
|
@ -80,7 +80,7 @@ class FuzzTarget:
|
|||
self.target_path = target_path
|
||||
self.target_name = os.path.basename(self.target_path)
|
||||
self.duration = int(duration)
|
||||
self.out_dir = out_dir
|
||||
self.workspace = workspace
|
||||
self.clusterfuzz_deployment = clusterfuzz_deployment
|
||||
self.config = config
|
||||
self.latest_corpus_path = None
|
||||
|
@ -92,17 +92,13 @@ class FuzzTarget:
|
|||
FuzzResult namedtuple with stacktrace and testcase if applicable.
|
||||
"""
|
||||
logging.info('Running fuzzer: %s.', self.target_name)
|
||||
command, _ = docker.get_base_docker_run_command(self.out_dir,
|
||||
command, _ = docker.get_base_docker_run_command(self.workspace,
|
||||
self.config.sanitizer,
|
||||
self.config.language)
|
||||
|
||||
# TODO(metzman): Stop using /out for artifacts and corpus. Use another
|
||||
# directory.
|
||||
# If corpus can be downloaded use it for fuzzing.
|
||||
self.latest_corpus_path = self.clusterfuzz_deployment.download_corpus(
|
||||
self.target_name, self.out_dir)
|
||||
command += docker.get_args_mapping_host_path_to_container(
|
||||
self.latest_corpus_path)
|
||||
self.target_name)
|
||||
command += ['-e', 'CORPUS_DIR=' + self.latest_corpus_path]
|
||||
|
||||
command += [
|
||||
|
@ -110,9 +106,13 @@ class FuzzTarget:
|
|||
'-c'
|
||||
]
|
||||
|
||||
options = LIBFUZZER_OPTIONS + ' -max_total_time=' + str(self.duration)
|
||||
options = LIBFUZZER_OPTIONS.copy() + [
|
||||
f'-max_total_time={self.duration}',
|
||||
# Make sure libFuzzer artifact files don't pollute $OUT.
|
||||
f'-artifact_prefix={self.workspace.artifacts}/'
|
||||
]
|
||||
options = ' '.join(options)
|
||||
run_fuzzer_command = f'run_fuzzer {self.target_name} {options}'
|
||||
|
||||
command.append(run_fuzzer_command)
|
||||
|
||||
logging.info('Running command: %s', ' '.join(command))
|
||||
|
@ -134,7 +134,7 @@ class FuzzTarget:
|
|||
|
||||
# Crash was discovered.
|
||||
logging.info('Fuzzer %s, ended before timeout.', self.target_name)
|
||||
testcase = self.get_testcase(stderr)
|
||||
testcase = get_testcase(stderr)
|
||||
if not testcase:
|
||||
logging.error(b'No testcase found in stacktrace: %s.', stderr)
|
||||
return FuzzResult(None, None, self.latest_corpus_path)
|
||||
|
@ -186,14 +186,13 @@ class FuzzTarget:
|
|||
Raises:
|
||||
ReproduceError if we can't attempt to reproduce the crash.
|
||||
"""
|
||||
|
||||
if not os.path.exists(target_path):
|
||||
raise ReproduceError(f'Target {target_path} not found.')
|
||||
|
||||
os.chmod(target_path, stat.S_IRWXO)
|
||||
|
||||
command, container = docker.get_base_docker_run_command(
|
||||
self.out_dir, self.config.sanitizer, self.config.language)
|
||||
self.workspace, self.config.sanitizer, self.config.language)
|
||||
if container:
|
||||
command += ['-e', f'TESTCASE={testcase}']
|
||||
else:
|
||||
|
@ -256,8 +255,7 @@ class FuzzTarget:
|
|||
can't be reproduced on an older ClusterFuzz build of the target."""
|
||||
if not os.path.exists(testcase):
|
||||
raise ReproduceError('Testcase %s not found.' % testcase)
|
||||
clusterfuzz_build_dir = self.clusterfuzz_deployment.download_latest_build(
|
||||
self.out_dir)
|
||||
clusterfuzz_build_dir = self.clusterfuzz_deployment.download_latest_build()
|
||||
if not clusterfuzz_build_dir:
|
||||
# Crash is reproducible on PR build and we can't test on a recent
|
||||
# ClusterFuzz/OSS-Fuzz build.
|
||||
|
@ -284,18 +282,17 @@ class FuzzTarget:
|
|||
'Code change (pr/commit) introduced crash.')
|
||||
return True
|
||||
|
||||
def get_testcase(self, error_bytes):
|
||||
"""Gets the file from a fuzzer run stacktrace.
|
||||
|
||||
Args:
|
||||
error_bytes: The bytes containing the output from the fuzzer.
|
||||
def get_testcase(stderr_bytes):
|
||||
"""Gets the file from a fuzzer run stacktrace.
|
||||
|
||||
Returns:
|
||||
The path to the testcase or None if not found.
|
||||
"""
|
||||
# TODO(metzman): Stop parsing and use libFuzzers' artifact_prefix option
|
||||
# instead.
|
||||
match = re.search(rb'\bTest unit written to \.\/([^\s]+)', error_bytes)
|
||||
if match:
|
||||
return os.path.join(self.out_dir, match.group(1).decode('utf-8'))
|
||||
return None
|
||||
Args:
|
||||
stderr_bytes: The bytes containing the output from the fuzzer.
|
||||
|
||||
Returns:
|
||||
The path to the testcase or None if not found.
|
||||
"""
|
||||
match = re.search(rb'\bTest unit written to (.+)', stderr_bytes)
|
||||
if match:
|
||||
return match.group(1).decode('utf-8')
|
||||
return None
|
||||
|
|
|
@ -23,6 +23,7 @@ import parameterized
|
|||
from pyfakefs import fake_filesystem_unittest
|
||||
|
||||
import clusterfuzz_deployment
|
||||
import docker
|
||||
import fuzz_target
|
||||
import test_helpers
|
||||
|
||||
|
@ -44,7 +45,11 @@ def _create_config(**kwargs):
|
|||
"""Creates a config object and then sets every attribute that is a key in
|
||||
|kwargs| to the corresponding value. Asserts that each key in |kwargs| is an
|
||||
attribute of Config."""
|
||||
defaults = {'is_github': True, 'project_name': EXAMPLE_PROJECT}
|
||||
defaults = {
|
||||
'is_github': True,
|
||||
'project_name': EXAMPLE_PROJECT,
|
||||
'workspace': '/workspace'
|
||||
}
|
||||
for default_key, default_value in defaults.items():
|
||||
if default_key not in kwargs:
|
||||
kwargs[default_key] = default_value
|
||||
|
@ -54,7 +59,8 @@ def _create_config(**kwargs):
|
|||
|
||||
def _create_deployment(**kwargs):
|
||||
config = _create_config(**kwargs)
|
||||
return clusterfuzz_deployment.get_clusterfuzz_deployment(config)
|
||||
workspace = docker.Workspace(config)
|
||||
return clusterfuzz_deployment.get_clusterfuzz_deployment(config, workspace)
|
||||
|
||||
|
||||
# TODO(metzman): Use patch from test_libs/helpers.py in clusterfuzz so that we
|
||||
|
@ -66,13 +72,14 @@ class IsReproducibleTest(fake_filesystem_unittest.TestCase):
|
|||
def setUp(self):
|
||||
"""Sets up example fuzz target to test is_reproducible method."""
|
||||
self.fuzz_target_name = 'fuzz-target'
|
||||
self.out_dir = '/example/outdir'
|
||||
self.fuzz_target_path = os.path.join(self.out_dir, self.fuzz_target_name)
|
||||
self.testcase_path = '/testcase'
|
||||
deployment = _create_deployment()
|
||||
self.workspace = deployment.workspace
|
||||
self.fuzz_target_path = os.path.join(self.workspace.out,
|
||||
self.fuzz_target_name)
|
||||
self.testcase_path = '/testcase'
|
||||
self.test_target = fuzz_target.FuzzTarget(self.fuzz_target_path,
|
||||
fuzz_target.REPRODUCE_ATTEMPTS,
|
||||
self.out_dir, deployment,
|
||||
self.workspace, deployment,
|
||||
deployment.config)
|
||||
|
||||
def test_reproducible(self, _):
|
||||
|
@ -87,8 +94,8 @@ class IsReproducibleTest(fake_filesystem_unittest.TestCase):
|
|||
'docker', 'run', '--rm', '--privileged', '--cap-add', 'SYS_PTRACE',
|
||||
'-e', 'FUZZING_ENGINE=libfuzzer', '-e', 'ARCHITECTURE=x86_64', '-e',
|
||||
'CIFUZZ=True', '-e', 'SANITIZER=' + self.test_target.config.sanitizer,
|
||||
'-e', 'FUZZING_LANGUAGE=' + self.test_target.config.language,
|
||||
'--volumes-from', 'container', '-e', 'OUT=' + self.out_dir, '-e',
|
||||
'-e', 'FUZZING_LANGUAGE=' + self.test_target.config.language, '-e',
|
||||
'OUT=' + self.workspace.out, '--volumes-from', 'container', '-e',
|
||||
'TESTCASE=' + self.testcase_path, '-t',
|
||||
'gcr.io/oss-fuzz-base/base-runner', 'reproduce',
|
||||
self.fuzz_target_name, '-runs=100'
|
||||
|
@ -136,32 +143,24 @@ class IsReproducibleTest(fake_filesystem_unittest.TestCase):
|
|||
class GetTestCaseTest(unittest.TestCase):
|
||||
"""Tests get_testcase."""
|
||||
|
||||
def setUp(self):
|
||||
"""Sets up example fuzz target to test get_testcase method."""
|
||||
deployment = _create_deployment()
|
||||
self.test_target = fuzz_target.FuzzTarget('/example/path', 10,
|
||||
'/example/outdir', deployment,
|
||||
deployment.config)
|
||||
|
||||
def test_valid_error_string(self):
|
||||
"""Tests that get_testcase returns the correct testcase give an error."""
|
||||
testcase_path = os.path.join(os.path.dirname(os.path.abspath(__file__)),
|
||||
'test_data', 'example_crash_fuzzer_output.txt')
|
||||
with open(testcase_path, 'rb') as test_fuzz_output:
|
||||
parsed_testcase = self.test_target.get_testcase(test_fuzz_output.read())
|
||||
self.assertEqual(
|
||||
parsed_testcase,
|
||||
'/example/outdir/crash-ad6700613693ef977ff3a8c8f4dae239c3dde6f5')
|
||||
parsed_testcase = fuzz_target.get_testcase(test_fuzz_output.read())
|
||||
self.assertEqual(parsed_testcase,
|
||||
'./crash-ad6700613693ef977ff3a8c8f4dae239c3dde6f5')
|
||||
|
||||
def test_invalid_error_string(self):
|
||||
"""Tests that get_testcase returns None with a bad error string."""
|
||||
self.assertIsNone(self.test_target.get_testcase(b''))
|
||||
self.assertIsNone(self.test_target.get_testcase(b' Example crash string.'))
|
||||
self.assertIsNone(fuzz_target.get_testcase(b''))
|
||||
self.assertIsNone(fuzz_target.get_testcase(b' Example crash string.'))
|
||||
|
||||
def test_encoding(self):
|
||||
"""Tests that get_testcase accepts bytes and returns a string."""
|
||||
fuzzer_output = b'\x8fTest unit written to ./crash-1'
|
||||
result = self.test_target.get_testcase(fuzzer_output)
|
||||
result = fuzz_target.get_testcase(fuzzer_output)
|
||||
self.assertTrue(isinstance(result, str))
|
||||
|
||||
|
||||
|
|
|
@ -18,9 +18,9 @@ import helper
|
|||
import docker
|
||||
|
||||
|
||||
def run_coverage_command(out_dir, config):
|
||||
def run_coverage_command(workspace, config):
|
||||
"""Runs the coverage command in base-runner to generate a coverage report."""
|
||||
docker_args, _ = docker.get_base_docker_run_args(out_dir, config.sanitizer,
|
||||
docker_args, _ = docker.get_base_docker_run_args(workspace, config.sanitizer,
|
||||
config.language)
|
||||
docker_args += [
|
||||
'-e', 'COVERAGE_EXTRA_ARGS=', '-e', 'HTTP_PORT=', '-t',
|
||||
|
@ -29,18 +29,18 @@ def run_coverage_command(out_dir, config):
|
|||
return helper.docker_run(docker_args)
|
||||
|
||||
|
||||
def download_corpora(out_dir, fuzz_target_paths, clusterfuzz_deployment):
|
||||
"""Downloads corpora to |out_dir| for the fuzz targets in |fuzz_target_paths|
|
||||
using clusterfuzz_deployment| to download corpora from ClusterFuzz/OSS-Fuzz"""
|
||||
def download_corpora(fuzz_target_paths, clusterfuzz_deployment):
|
||||
"""Downloads corpora for fuzz targets in |fuzz_target_paths| using
|
||||
clusterfuzz_deployment| to download corpora from ClusterFuzz/OSS-Fuzz."""
|
||||
# TODO(metzman): Download to /corpus dir.
|
||||
for target_path in fuzz_target_paths:
|
||||
target = os.path.basename(target_path)
|
||||
clusterfuzz_deployment.download_corpus(target, out_dir)
|
||||
clusterfuzz_deployment.download_corpus(target)
|
||||
|
||||
|
||||
def generate_coverage_report(fuzz_target_paths, out_dir, clusterfuzz_deployment,
|
||||
config):
|
||||
def generate_coverage_report(fuzz_target_paths, workspace,
|
||||
clusterfuzz_deployment, config):
|
||||
"""Generates a coverage report using Clang's source based coverage."""
|
||||
download_corpora(out_dir, fuzz_target_paths, clusterfuzz_deployment)
|
||||
run_coverage_command(out_dir, config)
|
||||
download_corpora(fuzz_target_paths, clusterfuzz_deployment)
|
||||
run_coverage_command(workspace, config)
|
||||
# TODO(metzman): Upload this build to the filestore.
|
||||
|
|
|
@ -30,17 +30,20 @@ class TestRunCoverageCommand(unittest.TestCase):
|
|||
@mock.patch('helper.docker_run')
|
||||
def test_run_coverage_command(self, mocked_docker_run): # pylint: disable=no-self-use
|
||||
"""Tests that run_coverage_command works as intended."""
|
||||
config = test_helpers.create_run_config(project_name=PROJECT,
|
||||
sanitizer=SANITIZER)
|
||||
workspace = test_helpers.create_workspace()
|
||||
expected_docker_args = [
|
||||
'--cap-add', 'SYS_PTRACE', '-e', 'FUZZING_ENGINE=libfuzzer', '-e',
|
||||
'ARCHITECTURE=x86_64', '-e', 'CIFUZZ=True', '-e',
|
||||
f'SANITIZER={SANITIZER}', '-e', 'FUZZING_LANGUAGE=c++', '-v',
|
||||
f'{OUT_DIR}:/out', '-e', 'COVERAGE_EXTRA_ARGS=', '-e', 'HTTP_PORT=',
|
||||
'-t', 'gcr.io/oss-fuzz-base/base-runner', 'coverage'
|
||||
f'SANITIZER={SANITIZER}', '-e', 'FUZZING_LANGUAGE=c++', '-e',
|
||||
'OUT=/workspace/build-out', '-v',
|
||||
f'{workspace.workspace}:{workspace.workspace}', '-e',
|
||||
'COVERAGE_EXTRA_ARGS=', '-e', 'HTTP_PORT=', '-t',
|
||||
'gcr.io/oss-fuzz-base/base-runner', 'coverage'
|
||||
]
|
||||
|
||||
config = test_helpers.create_run_config(project_name=PROJECT,
|
||||
sanitizer=SANITIZER)
|
||||
generate_coverage_report.run_coverage_command(OUT_DIR, config)
|
||||
generate_coverage_report.run_coverage_command(workspace, config)
|
||||
mocked_docker_run.assert_called_with(expected_docker_args)
|
||||
|
||||
|
||||
|
@ -51,10 +54,7 @@ class DownloadCorporaTest(unittest.TestCase):
|
|||
"""Tests that download_corpora works as intended."""
|
||||
clusterfuzz_deployment = mock.Mock()
|
||||
fuzz_target_paths = ['/path/to/fuzzer1', '/path/to/fuzzer2']
|
||||
expected_calls = [
|
||||
mock.call('fuzzer1', OUT_DIR),
|
||||
mock.call('fuzzer2', OUT_DIR)
|
||||
]
|
||||
generate_coverage_report.download_corpora(OUT_DIR, fuzz_target_paths,
|
||||
expected_calls = [mock.call('fuzzer1'), mock.call('fuzzer2')]
|
||||
generate_coverage_report.download_corpora(fuzz_target_paths,
|
||||
clusterfuzz_deployment)
|
||||
clusterfuzz_deployment.download_corpus.assert_has_calls(expected_calls)
|
||||
|
|
|
@ -20,6 +20,7 @@ import sys
|
|||
import time
|
||||
|
||||
import clusterfuzz_deployment
|
||||
import docker
|
||||
import fuzz_target
|
||||
import generate_coverage_report
|
||||
import stack_parser
|
||||
|
@ -42,16 +43,17 @@ class BaseFuzzTargetRunner:
|
|||
|
||||
def __init__(self, config):
|
||||
self.config = config
|
||||
self.workspace = docker.Workspace(config)
|
||||
self.clusterfuzz_deployment = (
|
||||
clusterfuzz_deployment.get_clusterfuzz_deployment(self.config))
|
||||
clusterfuzz_deployment.get_clusterfuzz_deployment(
|
||||
self.config, self.workspace))
|
||||
|
||||
# Set by the initialize method.
|
||||
self.out_dir = None
|
||||
self.fuzz_target_paths = None
|
||||
self.crashes_dir = None
|
||||
|
||||
def get_fuzz_targets(self):
|
||||
"""Returns fuzz targets in out_dir."""
|
||||
return utils.get_fuzz_targets(self.out_dir)
|
||||
"""Returns fuzz targets in out directory."""
|
||||
return utils.get_fuzz_targets(self.workspace.out)
|
||||
|
||||
def initialize(self):
|
||||
"""Initialization method. Must be called before calling run_fuzz_targets.
|
||||
|
@ -69,24 +71,23 @@ class BaseFuzzTargetRunner:
|
|||
self.config.fuzz_seconds)
|
||||
return False
|
||||
|
||||
self.out_dir = os.path.join(self.config.workspace, 'out')
|
||||
if not os.path.exists(self.out_dir):
|
||||
logging.error('Out directory: %s does not exist.', self.out_dir)
|
||||
if not os.path.exists(self.workspace.out):
|
||||
logging.error('Out directory: %s does not exist.', self.workspace.out)
|
||||
return False
|
||||
|
||||
self.crashes_dir = os.path.join(self.out_dir, 'artifacts')
|
||||
if not os.path.exists(self.crashes_dir):
|
||||
os.mkdir(self.crashes_dir)
|
||||
elif (not os.path.isdir(self.crashes_dir) or os.listdir(self.crashes_dir)):
|
||||
if not os.path.exists(self.workspace.artifacts):
|
||||
os.makedirs(self.workspace.artifacts)
|
||||
elif (not os.path.isdir(self.workspace.artifacts) or
|
||||
os.listdir(self.workspace.artifacts)):
|
||||
logging.error('Artifacts path: %s exists and is not an empty directory.',
|
||||
self.crashes_dir)
|
||||
self.workspace.artifacts)
|
||||
return False
|
||||
|
||||
self.fuzz_target_paths = self.get_fuzz_targets()
|
||||
logging.info('Fuzz targets: %s', self.fuzz_target_paths)
|
||||
if not self.fuzz_target_paths:
|
||||
logging.error('No fuzz targets were found in out directory: %s.',
|
||||
self.out_dir)
|
||||
self.workspace.out)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
@ -110,11 +111,11 @@ class BaseFuzzTargetRunner:
|
|||
|fuzz_target|."""
|
||||
artifact_name = (f'{target.target_name}-{self.config.sanitizer}-'
|
||||
f'{artifact_name}')
|
||||
return os.path.join(self.crashes_dir, artifact_name)
|
||||
return os.path.join(self.workspace.artifacts, artifact_name)
|
||||
|
||||
def create_fuzz_target_obj(self, target_path, run_seconds):
|
||||
"""Returns a fuzz target object."""
|
||||
return fuzz_target.FuzzTarget(target_path, run_seconds, self.out_dir,
|
||||
return fuzz_target.FuzzTarget(target_path, run_seconds, self.workspace,
|
||||
self.clusterfuzz_deployment, self.config)
|
||||
|
||||
def run_fuzz_targets(self):
|
||||
|
@ -173,19 +174,19 @@ class CoverageTargetRunner(BaseFuzzTargetRunner):
|
|||
raise NotImplementedError('Not implemented for CoverageTargetRunner.')
|
||||
|
||||
def get_fuzz_targets(self):
|
||||
"""Returns fuzz targets in out_dir."""
|
||||
"""Returns fuzz targets in out directory."""
|
||||
# We only want fuzz targets from the root because during the coverage build,
|
||||
# a lot of the image's filesystem is copied into /out for the purpose of
|
||||
# generating coverage reports.
|
||||
# TOOD(metzman): Figure out if top_level_only should be the only behavior
|
||||
# for this function.
|
||||
return utils.get_fuzz_targets(self.out_dir, top_level_only=True)
|
||||
return utils.get_fuzz_targets(self.workspace.out, top_level_only=True)
|
||||
|
||||
def run_fuzz_targets(self):
|
||||
"""Generates a coverage report. Always returns False since it never finds
|
||||
any bugs."""
|
||||
generate_coverage_report.generate_coverage_report(
|
||||
self.fuzz_target_paths, self.out_dir, self.clusterfuzz_deployment,
|
||||
self.fuzz_target_paths, self.workspace, self.clusterfuzz_deployment,
|
||||
self.config)
|
||||
return False
|
||||
|
||||
|
@ -238,38 +239,20 @@ class BatchFuzzTargetRunner(BaseFuzzTargetRunner):
|
|||
def run_fuzz_targets(self):
|
||||
result = super().run_fuzz_targets()
|
||||
|
||||
self.clusterfuzz_deployment.upload_crashes(self.crashes_dir)
|
||||
self.clusterfuzz_deployment.upload_crashes()
|
||||
|
||||
# We want to upload the build to the filestore after we do batch fuzzing.
|
||||
# There are some problems with this. First, we don't want to upload the
|
||||
# build before fuzzing, because if we download the latest build, we will
|
||||
# consider the build we just uploaded to be the latest even though it
|
||||
# shouldn't be (we really intend to download the build before the curent
|
||||
# one.
|
||||
# Second, the out directory is mounted into the runnner container and is
|
||||
# used to pass the runner corpora, old builds and for the runner to pass the
|
||||
# host testcases. Thus, we will upload the build after fuzzing. But before
|
||||
# we zip the out directory we will remove these extra things that are now in
|
||||
# out.
|
||||
# There are some is a problem with this. We don't want to upload the build
|
||||
# before fuzzing, because if we download the latest build, we will consider
|
||||
# the build we just uploaded to be the latest even though it shouldn't be
|
||||
# (we really intend to download the build before the curent one.
|
||||
# TODO(metzman): We should really be uploading latest build in build_fuzzers
|
||||
# before we remove unaffected fuzzers. Otherwise, we can lose fuzzers. This
|
||||
# is probably more of a theoretical concern since in batch fuzzing, there is
|
||||
# no code change and thus no fuzzers that are removed, but it's inelegant to
|
||||
# put this here.
|
||||
# TODO(metzman): Don't pollute self.out_dir like this.
|
||||
|
||||
for directory in [
|
||||
self.clusterfuzz_deployment.get_corpus_dir(self.out_dir),
|
||||
# This is the directory of the ClusterFuzz build, not the build we just
|
||||
# did.
|
||||
self.clusterfuzz_deployment.get_build_dir(self.out_dir),
|
||||
self.crashes_dir,
|
||||
]:
|
||||
if os.path.exists(directory):
|
||||
shutil.rmtree(directory)
|
||||
|
||||
self.clusterfuzz_deployment.upload_latest_build(self.out_dir)
|
||||
|
||||
self.clusterfuzz_deployment.upload_latest_build()
|
||||
return result
|
||||
|
||||
|
||||
|
|
|
@ -121,7 +121,7 @@ class BaseFuzzTargetRunnerTest(unittest.TestCase):
|
|||
expected_error_args = ('Fuzz_seconds argument must be greater than 1, '
|
||||
'but was: %s.', fuzz_seconds)
|
||||
with tempfile.TemporaryDirectory() as tmp_dir:
|
||||
out_path = os.path.join(tmp_dir, 'out')
|
||||
out_path = os.path.join(tmp_dir, 'build-out')
|
||||
os.mkdir(out_path)
|
||||
with mock.patch('utils.get_fuzz_targets') as mocked_get_fuzz_targets:
|
||||
mocked_get_fuzz_targets.return_value = [
|
||||
|
@ -134,16 +134,17 @@ class BaseFuzzTargetRunnerTest(unittest.TestCase):
|
|||
def test_initialize_no_out_dir(self):
|
||||
"""Tests initialize fails with no out dir."""
|
||||
with tempfile.TemporaryDirectory() as tmp_dir:
|
||||
out_path = os.path.join(tmp_dir, 'out')
|
||||
out_path = os.path.join(tmp_dir, 'build-out')
|
||||
expected_error_args = ('Out directory: %s does not exist.', out_path)
|
||||
self._test_initialize_fail(expected_error_args, workspace=tmp_dir)
|
||||
|
||||
def test_initialize_nonempty_artifacts(self):
|
||||
"""Tests initialize with a file artifacts path."""
|
||||
with tempfile.TemporaryDirectory() as tmp_dir:
|
||||
out_path = os.path.join(tmp_dir, 'out')
|
||||
out_path = os.path.join(tmp_dir, 'build-out')
|
||||
os.mkdir(out_path)
|
||||
artifacts_path = os.path.join(out_path, 'artifacts')
|
||||
os.makedirs(os.path.join(tmp_dir, 'out'))
|
||||
artifacts_path = os.path.join(tmp_dir, 'out', 'artifacts')
|
||||
with open(artifacts_path, 'w') as artifacts_handle:
|
||||
artifacts_handle.write('fake')
|
||||
expected_error_args = (
|
||||
|
@ -154,8 +155,9 @@ class BaseFuzzTargetRunnerTest(unittest.TestCase):
|
|||
def test_initialize_bad_artifacts(self):
|
||||
"""Tests initialize with a non-empty artifacts path."""
|
||||
with tempfile.TemporaryDirectory() as tmp_dir:
|
||||
out_path = os.path.join(tmp_dir, 'out')
|
||||
artifacts_path = os.path.join(out_path, 'artifacts')
|
||||
out_path = os.path.join(tmp_dir, 'build-out')
|
||||
os.mkdir(out_path)
|
||||
artifacts_path = os.path.join(tmp_dir, 'out', 'artifacts')
|
||||
os.makedirs(artifacts_path)
|
||||
artifact_path = os.path.join(artifacts_path, 'artifact')
|
||||
with open(artifact_path, 'w') as artifact_handle:
|
||||
|
@ -172,8 +174,9 @@ class BaseFuzzTargetRunnerTest(unittest.TestCase):
|
|||
"""Tests initialize with an empty artifacts dir."""
|
||||
mocked_get_fuzz_targets.return_value = ['fuzz-target']
|
||||
with tempfile.TemporaryDirectory() as tmp_dir:
|
||||
out_path = os.path.join(tmp_dir, 'out')
|
||||
artifacts_path = os.path.join(out_path, 'artifacts')
|
||||
out_path = os.path.join(tmp_dir, 'build-out')
|
||||
os.mkdir(out_path)
|
||||
artifacts_path = os.path.join(tmp_dir, 'out', 'artifacts')
|
||||
os.makedirs(artifacts_path)
|
||||
runner = self._create_runner(workspace=tmp_dir)
|
||||
self.assertTrue(runner.initialize())
|
||||
|
@ -187,17 +190,17 @@ class BaseFuzzTargetRunnerTest(unittest.TestCase):
|
|||
"""Tests initialize with no artifacts dir (the expected setting)."""
|
||||
mocked_get_fuzz_targets.return_value = ['fuzz-target']
|
||||
with tempfile.TemporaryDirectory() as tmp_dir:
|
||||
out_path = os.path.join(tmp_dir, 'out')
|
||||
os.makedirs(out_path)
|
||||
out_path = os.path.join(tmp_dir, 'build-out')
|
||||
os.mkdir(out_path)
|
||||
runner = self._create_runner(workspace=tmp_dir)
|
||||
self.assertTrue(runner.initialize())
|
||||
mocked_log_error.assert_not_called()
|
||||
self.assertTrue(os.path.isdir(os.path.join(out_path, 'artifacts')))
|
||||
self.assertTrue(os.path.isdir(os.path.join(tmp_dir, 'out', 'artifacts')))
|
||||
|
||||
def test_initialize_no_fuzz_targets(self):
|
||||
"""Tests initialize with no fuzz targets."""
|
||||
with tempfile.TemporaryDirectory() as tmp_dir:
|
||||
out_path = os.path.join(tmp_dir, 'out')
|
||||
out_path = os.path.join(tmp_dir, 'build-out')
|
||||
os.makedirs(out_path)
|
||||
expected_error_args = ('No fuzz targets were found in out directory: %s.',
|
||||
out_path)
|
||||
|
@ -205,18 +208,21 @@ class BaseFuzzTargetRunnerTest(unittest.TestCase):
|
|||
|
||||
def test_get_fuzz_target_artifact(self):
|
||||
"""Tests that get_fuzz_target_artifact works as intended."""
|
||||
runner = self._create_runner()
|
||||
crashes_dir = 'crashes-dir'
|
||||
runner.crashes_dir = crashes_dir
|
||||
artifact_name = 'artifact-name'
|
||||
target = mock.MagicMock()
|
||||
target_name = 'target_name'
|
||||
target.target_name = target_name
|
||||
fuzz_target_artifact = runner.get_fuzz_target_artifact(
|
||||
target, artifact_name)
|
||||
expected_fuzz_target_artifact = (
|
||||
'crashes-dir/target_name-address-artifact-name')
|
||||
self.assertEqual(fuzz_target_artifact, expected_fuzz_target_artifact)
|
||||
with tempfile.TemporaryDirectory() as tmp_dir:
|
||||
runner = self._create_runner(workspace=tmp_dir)
|
||||
crashes_dir = 'crashes-dir'
|
||||
runner.crashes_dir = crashes_dir
|
||||
artifact_name = 'artifact-name'
|
||||
target = mock.MagicMock()
|
||||
target_name = 'target_name'
|
||||
target.target_name = target_name
|
||||
|
||||
fuzz_target_artifact = runner.get_fuzz_target_artifact(
|
||||
target, artifact_name)
|
||||
expected_fuzz_target_artifact = os.path.join(
|
||||
tmp_dir, 'out', 'artifacts', 'target_name-address-artifact-name')
|
||||
|
||||
self.assertEqual(fuzz_target_artifact, expected_fuzz_target_artifact)
|
||||
|
||||
|
||||
class CiFuzzTargetRunnerTest(fake_filesystem_unittest.TestCase):
|
||||
|
@ -233,7 +239,7 @@ class CiFuzzTargetRunnerTest(fake_filesystem_unittest.TestCase):
|
|||
mocked_get_fuzz_targets):
|
||||
"""Tests that run_fuzz_targets quits on the first crash it finds."""
|
||||
workspace = 'workspace'
|
||||
out_path = os.path.join(workspace, 'out')
|
||||
out_path = os.path.join(workspace, 'build-out')
|
||||
self.fs.create_dir(out_path)
|
||||
config = test_helpers.create_run_config(fuzz_seconds=FUZZ_SECONDS,
|
||||
workspace=workspace,
|
||||
|
@ -253,7 +259,8 @@ class CiFuzzTargetRunnerTest(fake_filesystem_unittest.TestCase):
|
|||
magic_mock.target_name = 'target1'
|
||||
mocked_create_fuzz_target_obj.return_value = magic_mock
|
||||
self.assertTrue(runner.run_fuzz_targets())
|
||||
self.assertIn('target1-address-testcase', os.listdir(runner.crashes_dir))
|
||||
self.assertIn('target1-address-testcase',
|
||||
os.listdir(runner.workspace.artifacts))
|
||||
self.assertEqual(mocked_run_fuzz_target.call_count, 1)
|
||||
|
||||
|
||||
|
@ -265,11 +272,11 @@ class BatchFuzzTargetRunnerTest(fake_filesystem_unittest.TestCase):
|
|||
|
||||
def setUp(self):
|
||||
self.setUpPyfakefs()
|
||||
self.out_dir = os.path.join(self.WORKSPACE, 'out')
|
||||
self.fs.create_dir(self.out_dir)
|
||||
self.testcase1 = os.path.join(self.WORKSPACE, 'testcase-aaa')
|
||||
out_dir = os.path.join(self.WORKSPACE, 'build-out')
|
||||
self.fs.create_dir(out_dir)
|
||||
self.testcase1 = os.path.join(out_dir, 'testcase-aaa')
|
||||
self.fs.create_file(self.testcase1)
|
||||
self.testcase2 = os.path.join(self.WORKSPACE, 'testcase-bbb')
|
||||
self.testcase2 = os.path.join(out_dir, 'testcase-bbb')
|
||||
self.fs.create_file(self.testcase2)
|
||||
self.config = test_helpers.create_run_config(fuzz_seconds=FUZZ_SECONDS,
|
||||
workspace=self.WORKSPACE,
|
||||
|
@ -317,32 +324,9 @@ class BatchFuzzTargetRunnerTest(fake_filesystem_unittest.TestCase):
|
|||
self, mocked_upload_crashes, mocked_upload_latest_build, _):
|
||||
"""Tests that run_fuzz_targets uploads crashes and builds correctly."""
|
||||
runner = run_fuzzers.BatchFuzzTargetRunner(self.config)
|
||||
# TODO(metzman): Don't rely on this failing gracefully.
|
||||
runner.initialize()
|
||||
|
||||
expected_crashes_dir = 'workspace/out/artifacts'
|
||||
|
||||
def mock_upload_crashes(crashes_dir):
|
||||
self.assertEqual(crashes_dir, expected_crashes_dir)
|
||||
# Ensure it wasn't deleted first.
|
||||
self.assertTrue(os.path.exists(crashes_dir))
|
||||
|
||||
mocked_upload_crashes.side_effect = mock_upload_crashes
|
||||
|
||||
expected_out_dir = 'workspace/out'
|
||||
expected_build_dir = 'workspace/out/cifuzz-latest-build'
|
||||
expected_corpus_dir = 'workspace/out/cifuzz-corpus'
|
||||
self.fs.create_dir(expected_build_dir)
|
||||
self.fs.create_dir(expected_corpus_dir)
|
||||
|
||||
def mock_upload_latest_build(out_dir):
|
||||
self.assertEqual(out_dir, expected_out_dir)
|
||||
# Ensure these were deleted before this function call.
|
||||
self.assertFalse(os.path.exists(expected_crashes_dir))
|
||||
self.assertFalse(os.path.exists(expected_build_dir))
|
||||
self.assertFalse(os.path.exists(expected_corpus_dir))
|
||||
|
||||
mocked_upload_latest_build.side_effect = mock_upload_latest_build
|
||||
|
||||
self.assertFalse(runner.run_fuzz_targets())
|
||||
self.assertEqual(mocked_upload_crashes.call_count, 1)
|
||||
self.assertEqual(mocked_upload_latest_build.call_count, 1)
|
||||
|
@ -362,7 +346,6 @@ class CoverageReportIntegrationTest(unittest.TestCase):
|
|||
generation."""
|
||||
|
||||
with tempfile.TemporaryDirectory() as workspace:
|
||||
out_dir = os.path.join(workspace, 'out')
|
||||
try:
|
||||
# Do coverage build.
|
||||
build_config = test_helpers.create_build_config(
|
||||
|
@ -388,8 +371,8 @@ class CoverageReportIntegrationTest(unittest.TestCase):
|
|||
TEST_DATA_PATH, 'example_coverage_report_summary.json')
|
||||
with open(expected_summary_path) as file_handle:
|
||||
expected_summary = json.loads(file_handle.read())
|
||||
actual_summary_path = os.path.join(out_dir, 'report', 'linux',
|
||||
'summary.json')
|
||||
actual_summary_path = os.path.join(workspace, 'build-out', 'report',
|
||||
'linux', 'summary.json')
|
||||
with open(actual_summary_path) as file_handle:
|
||||
actual_summary = json.loads(file_handle.read())
|
||||
self.assertEqual(expected_summary, actual_summary)
|
||||
|
@ -427,8 +410,6 @@ class RunAddressFuzzersIntegrationTest(RunFuzzerIntegrationTestMixin,
|
|||
project_name=EXAMPLE_PROJECT)
|
||||
result = run_fuzzers.run_fuzzers(config)
|
||||
self.assertEqual(result, run_fuzzers.RunFuzzersResult.BUG_FOUND)
|
||||
build_dir = os.path.join(workspace, 'out', self.BUILD_DIR_NAME)
|
||||
self.assertNotEqual(0, len(os.listdir(build_dir)))
|
||||
|
||||
@mock.patch('fuzz_target.FuzzTarget.is_reproducible',
|
||||
side_effect=[True, True])
|
||||
|
@ -438,18 +419,15 @@ class RunAddressFuzzersIntegrationTest(RunFuzzerIntegrationTestMixin,
|
|||
workspace = os.path.join(tmp_dir, 'workspace')
|
||||
shutil.copytree(TEST_DATA_PATH, workspace)
|
||||
config = test_helpers.create_run_config(fuzz_seconds=FUZZ_SECONDS,
|
||||
workspace=TEST_DATA_PATH,
|
||||
workspace=workspace,
|
||||
project_name=EXAMPLE_PROJECT)
|
||||
result = run_fuzzers.run_fuzzers(config)
|
||||
self.assertEqual(result, run_fuzzers.RunFuzzersResult.NO_BUG_FOUND)
|
||||
build_dir = os.path.join(TEST_DATA_PATH, 'out', self.BUILD_DIR_NAME)
|
||||
self.assertTrue(os.path.exists(build_dir))
|
||||
self.assertNotEqual(0, len(os.listdir(build_dir)))
|
||||
|
||||
def test_invalid_build(self):
|
||||
"""Tests run_fuzzers with an invalid ASAN build."""
|
||||
with tempfile.TemporaryDirectory() as tmp_dir:
|
||||
out_path = os.path.join(tmp_dir, 'out')
|
||||
out_path = os.path.join(tmp_dir, 'build-out')
|
||||
os.mkdir(out_path)
|
||||
config = test_helpers.create_run_config(fuzz_seconds=FUZZ_SECONDS,
|
||||
workspace=tmp_dir,
|
||||
|
|
|
@ -20,6 +20,7 @@ import tempfile
|
|||
from unittest import mock
|
||||
|
||||
import config_utils
|
||||
import docker
|
||||
|
||||
|
||||
def _create_config(config_cls, **kwargs):
|
||||
|
@ -48,6 +49,13 @@ def create_run_config(**kwargs):
|
|||
return _create_config(config_utils.RunFuzzersConfig, **kwargs)
|
||||
|
||||
|
||||
def create_workspace(workspace_path='/workspace'):
|
||||
"""Returns a workspace located at |workspace_path| ('/workspace' by
|
||||
default)."""
|
||||
config = create_run_config(workspace=workspace_path)
|
||||
return docker.Workspace(config)
|
||||
|
||||
|
||||
def patch_environ(testcase_obj, env=None):
|
||||
"""Patch environment."""
|
||||
if env is None:
|
||||
|
|
|
@ -24,7 +24,7 @@ import helper
|
|||
EXAMPLE_PROJECT = 'example'
|
||||
|
||||
TEST_OUT_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)),
|
||||
'cifuzz', 'test_data', 'out')
|
||||
'cifuzz', 'test_data', 'build-out')
|
||||
|
||||
|
||||
class IsFuzzTargetLocalTest(unittest.TestCase):
|
||||
|
|
Loading…
Reference in New Issue