[ClusterFuzzLite] Support GCB and gsutil/gcs as filestore. (#6629)

* add gsutil filestore

* lint

* Fix

* Add build image script

* get gcb fuzzing working

* fmt and fix config_utils_test

* Check that crashes are uploaded

* Add no_filestore

* fix test

* fix tests

* fix

* Print crash URL

* Fix

* fix

* fmt

* lnt

* fix

* fmt
This commit is contained in:
jonathanmetzman 2021-10-27 10:00:04 -04:00 committed by GitHub
parent d951635512
commit b77a55b9b4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 313 additions and 60 deletions

View File

@ -1,4 +1,5 @@
.git .git
.venv
infra/cifuzz/test_data/* infra/cifuzz/test_data/*
docs/* docs/*
@ -8,4 +9,6 @@ docs/*
build build
*~ *~
.DS_Store .DS_Store
*.swp *.swp
.pytest_cache
*__pycache__/*

28
infra/cifuzz/build-images.sh Executable file
View File

@ -0,0 +1,28 @@
#! /bin/bash -eux
# 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.
# Script for building the docker images for cifuzz.
CIFUZZ_DIR=$(dirname "$0")
CIFUZZ_DIR=$(realpath $CIFUZZ_DIR)
INFRA_DIR=$(realpath $CIFUZZ_DIR/..)
OSS_FUZZ_ROOT=$(realpath $INFRA_DIR/..)
# Build cifuzz-base.
docker build --tag gcr.io/oss-fuzz-base/cifuzz-base --file $CIFUZZ_DIR/cifuzz-base/Dockerfile $OSS_FUZZ_ROOT
# Build run-fuzzers and build-fuzzers images.
docker build --tag gcr.io/oss-fuzz-base/cifuzz-build-fuzzers:v1 --file $INFRA_DIR/build_fuzzers.Dockerfile $INFRA_DIR
docker build --tag gcr.io/oss-fuzz-base/cifuzz-run-fuzzers:v1 --file $INFRA_DIR/run_fuzzers.Dockerfile $INFRA_DIR

View File

@ -201,6 +201,7 @@ class BuildFuzzersIntegrationTest(unittest.TestCase):
project_repo_name=project_repo_name, project_repo_name=project_repo_name,
workspace=self.workspace, workspace=self.workspace,
git_url=git_url, git_url=git_url,
filestore='no_filestore',
commit_sha='HEAD', commit_sha='HEAD',
project_src_path=project_src_path, project_src_path=project_src_path,
base_commit='HEAD^1') base_commit='HEAD^1')

View File

@ -20,8 +20,17 @@ RUN apt-get update && \
apt-get install -y systemd && \ apt-get install -y systemd && \
apt-get install -y --no-install-recommends nodejs npm && \ apt-get install -y --no-install-recommends nodejs npm && \
wget https://download.docker.com/linux/ubuntu/dists/focal/pool/stable/amd64/docker-ce-cli_20.10.8~3-0~ubuntu-focal_amd64.deb -O /tmp/docker-ce.deb && \ wget https://download.docker.com/linux/ubuntu/dists/focal/pool/stable/amd64/docker-ce-cli_20.10.8~3-0~ubuntu-focal_amd64.deb -O /tmp/docker-ce.deb && \
dpkg -i /tmp/docker-ce.deb && rm /tmp/docker-ce.deb dpkg -i /tmp/docker-ce.deb && \
rm /tmp/docker-ce.deb && \
mkdir -p /opt/gcloud && \
wget -qO- https://dl.google.com/dl/cloudsdk/release/google-cloud-sdk.tar.gz | tar zxv -C /opt/gcloud && \
/opt/gcloud/google-cloud-sdk/install.sh --usage-reporting=false --bash-completion=false --disable-installation-options && \
apt-get -y install gcc python3-dev && \
pip3 install -U crcmod && \
apt-get autoremove -y gcc python3-dev
ENV PATH=/opt/gcloud/google-cloud-sdk/bin/:$PATH
ENV OSS_FUZZ_ROOT=/opt/oss-fuzz ENV OSS_FUZZ_ROOT=/opt/oss-fuzz
ADD . ${OSS_FUZZ_ROOT} ADD . ${OSS_FUZZ_ROOT}
RUN python3 -m pip install -r ${OSS_FUZZ_ROOT}/infra/cifuzz/requirements.txt RUN python3 -m pip install -r ${OSS_FUZZ_ROOT}/infra/cifuzz/requirements.txt

View File

@ -39,6 +39,8 @@ class EndToEndTest(unittest.TestCase):
"""Simple end-to-end test using run_cifuzz.main().""" """Simple end-to-end test using run_cifuzz.main()."""
os.environ['REPOSITORY'] = 'external-project' os.environ['REPOSITORY'] = 'external-project'
os.environ['PROJECT_SRC_PATH'] = EXTERNAL_PROJECT_PATH os.environ['PROJECT_SRC_PATH'] = EXTERNAL_PROJECT_PATH
os.environ['FILESTORE'] = 'no_filestore'
os.environ['NO_CLUSTERFUZZ_DEPLOYMENT'] = 'True'
with test_helpers.docker_temp_dir() as temp_dir: with test_helpers.docker_temp_dir() as temp_dir:
os.environ['WORKSPACE'] = temp_dir os.environ['WORKSPACE'] = temp_dir

View File

@ -100,7 +100,7 @@ class ClusterFuzzLite(BaseClusterFuzzDeployment):
# called if multiple bugs are found. # called if multiple bugs are found.
return self.workspace.clusterfuzz_build return self.workspace.clusterfuzz_build
repo_dir = self.ci_system.repo_dir() repo_dir = self.ci_system.repo_dir
if not repo_dir: if not repo_dir:
raise RuntimeError('Repo checkout does not exist.') raise RuntimeError('Repo checkout does not exist.')
@ -355,20 +355,19 @@ class NoClusterFuzzDeployment(BaseClusterFuzzDeployment):
_PLATFORM_CLUSTERFUZZ_DEPLOYMENT_MAPPING = { _PLATFORM_CLUSTERFUZZ_DEPLOYMENT_MAPPING = {
config_utils.BaseConfig.Platform.INTERNAL_GENERIC_CI: config_utils.BaseConfig.Platform.INTERNAL_GENERIC_CI: OSSFuzz,
OSSFuzz, config_utils.BaseConfig.Platform.INTERNAL_GITHUB: OSSFuzz,
config_utils.BaseConfig.Platform.INTERNAL_GITHUB: config_utils.BaseConfig.Platform.EXTERNAL_GENERIC_CI: ClusterFuzzLite,
OSSFuzz, config_utils.BaseConfig.Platform.EXTERNAL_GITHUB: ClusterFuzzLite,
config_utils.BaseConfig.Platform.EXTERNAL_GENERIC_CI:
NoClusterFuzzDeployment,
config_utils.BaseConfig.Platform.EXTERNAL_GITHUB:
ClusterFuzzLite,
} }
def get_clusterfuzz_deployment(config, workspace): def get_clusterfuzz_deployment(config, workspace):
"""Returns object reprsenting deployment of ClusterFuzz used by |config|.""" """Returns object reprsenting deployment of ClusterFuzz used by |config|."""
deployment_cls = _PLATFORM_CLUSTERFUZZ_DEPLOYMENT_MAPPING[config.platform] deployment_cls = _PLATFORM_CLUSTERFUZZ_DEPLOYMENT_MAPPING[config.platform]
if config.no_clusterfuzz_deployment:
logging.info('Overriding ClusterFuzzDeployment. Using None.')
deployment_cls = NoClusterFuzzDeployment
result = deployment_cls(config, workspace) result = deployment_cls(config, workspace)
logging.info('ClusterFuzzDeployment: %s.', result) logging.info('ClusterFuzzDeployment: %s.', result)
return result return result

View File

@ -132,6 +132,7 @@ class ClusterFuzzLiteTest(fake_filesystem_unittest.TestCase):
self.setUpPyfakefs() self.setUpPyfakefs()
self.deployment = _create_deployment(run_fuzzers_mode='batch', self.deployment = _create_deployment(run_fuzzers_mode='batch',
oss_fuzz_project_name='', oss_fuzz_project_name='',
cloud_bucket='gs://bucket',
is_github=True) is_github=True)
self.corpus_dir = os.path.join(self.deployment.workspace.corpora, self.corpus_dir = os.path.join(self.deployment.workspace.corpora,
EXAMPLE_FUZZER) EXAMPLE_FUZZER)
@ -157,7 +158,7 @@ class ClusterFuzzLiteTest(fake_filesystem_unittest.TestCase):
side_effect=[False, True]) side_effect=[False, True])
@mock.patch('repo_manager.RepoManager.get_commit_list', @mock.patch('repo_manager.RepoManager.get_commit_list',
return_value=['commit1', 'commit2']) return_value=['commit1', 'commit2'])
@mock.patch('continuous_integration.BaseCi.repo_dir', @mock.patch('continuous_integration.GithubCiMixin.repo_dir',
return_value='/path/to/repo') return_value='/path/to/repo')
def test_download_latest_build(self, mock_repo_dir, mock_get_commit_list, def test_download_latest_build(self, mock_repo_dir, mock_get_commit_list,
mock_download_build): mock_download_build):
@ -173,7 +174,7 @@ class ClusterFuzzLiteTest(fake_filesystem_unittest.TestCase):
side_effect=Exception) side_effect=Exception)
@mock.patch('repo_manager.RepoManager.get_commit_list', @mock.patch('repo_manager.RepoManager.get_commit_list',
return_value=['commit1', 'commit2']) return_value=['commit1', 'commit2'])
@mock.patch('continuous_integration.BaseCi.repo_dir', @mock.patch('continuous_integration.GithubCiMixin.repo_dir',
return_value='/path/to/repo') return_value='/path/to/repo')
def test_download_latest_build_fail(self, mock_repo_dir, mock_get_commit_list, def test_download_latest_build_fail(self, mock_repo_dir, mock_get_commit_list,
_): _):
@ -195,10 +196,13 @@ class NoClusterFuzzDeploymentTest(fake_filesystem_unittest.TestCase):
def setUp(self): def setUp(self):
self.setUpPyfakefs() self.setUpPyfakefs()
config = test_helpers.create_run_config(workspace=WORKSPACE, config = test_helpers.create_run_config(workspace=WORKSPACE,
is_github=False) is_github=False,
filestore='no_filestore',
no_clusterfuzz_deployment=True)
workspace = workspace_utils.Workspace(config) workspace = workspace_utils.Workspace(config)
self.deployment = clusterfuzz_deployment.get_clusterfuzz_deployment( self.deployment = clusterfuzz_deployment.get_clusterfuzz_deployment(
config, workspace) config, workspace)
self.corpus_dir = os.path.join(workspace.corpora, EXAMPLE_FUZZER) self.corpus_dir = os.path.join(workspace.corpora, EXAMPLE_FUZZER)
@mock.patch('logging.info') @mock.patch('logging.info')
@ -241,7 +245,7 @@ class GetClusterFuzzDeploymentTest(unittest.TestCase):
(config_utils.BaseConfig.Platform.INTERNAL_GITHUB, (config_utils.BaseConfig.Platform.INTERNAL_GITHUB,
clusterfuzz_deployment.OSSFuzz), clusterfuzz_deployment.OSSFuzz),
(config_utils.BaseConfig.Platform.EXTERNAL_GENERIC_CI, (config_utils.BaseConfig.Platform.EXTERNAL_GENERIC_CI,
clusterfuzz_deployment.NoClusterFuzzDeployment), clusterfuzz_deployment.ClusterFuzzLite),
(config_utils.BaseConfig.Platform.EXTERNAL_GITHUB, (config_utils.BaseConfig.Platform.EXTERNAL_GITHUB,
clusterfuzz_deployment.ClusterFuzzLite), clusterfuzz_deployment.ClusterFuzzLite),
]) ])

View File

@ -236,7 +236,12 @@ class BaseConfig:
self.git_store_branch = os.environ.get('GIT_STORE_BRANCH') self.git_store_branch = os.environ.get('GIT_STORE_BRANCH')
self.git_store_branch_coverage = os.environ.get('GIT_STORE_BRANCH_COVERAGE', self.git_store_branch_coverage = os.environ.get('GIT_STORE_BRANCH_COVERAGE',
self.git_store_branch) self.git_store_branch)
self.project_src_path = self._ci_env.project_src_path
self.docker_in_docker = os.environ.get('DOCKER_IN_DOCKER') self.docker_in_docker = os.environ.get('DOCKER_IN_DOCKER')
self.filestore = os.environ.get('FILESTORE')
self.cloud_bucket = os.environ.get('CLOUD_BUCKET')
self.no_clusterfuzz_deployment = os.environ.get('NO_CLUSTERFUZZ_DEPLOYMENT',
False)
# TODO(metzman): Fix tests to create valid configurations and get rid of # TODO(metzman): Fix tests to create valid configurations and get rid of
# CIFUZZ_TEST here and in presubmit.py. # CIFUZZ_TEST here and in presubmit.py.
@ -261,6 +266,10 @@ class BaseConfig:
constants.LANGUAGES) constants.LANGUAGES)
return False return False
if not self.project_repo_name:
logging.error('Must set REPOSITORY.')
return False
return True return True
@property @property
@ -361,7 +370,6 @@ class BuildFuzzersConfig(BaseConfig):
self._get_config_from_event_path(event) self._get_config_from_event_path(event)
self.base_ref = os.getenv('GITHUB_BASE_REF') self.base_ref = os.getenv('GITHUB_BASE_REF')
self.project_src_path = self._ci_env.project_src_path
self.allowed_broken_targets_percentage = os.getenv( self.allowed_broken_targets_percentage = os.getenv(
'ALLOWED_BROKEN_TARGETS_PERCENTAGE') 'ALLOWED_BROKEN_TARGETS_PERCENTAGE')

View File

@ -91,6 +91,7 @@ class BaseConfigTest(unittest.TestCase):
"""Tests that validate returns True if config is valid.""" """Tests that validate returns True if config is valid."""
os.environ['OSS_FUZZ_PROJECT_NAME'] = 'example' os.environ['OSS_FUZZ_PROJECT_NAME'] = 'example'
os.environ['WORKSPACE'] = '/workspace' os.environ['WORKSPACE'] = '/workspace'
os.environ['REPOSITORY'] = 'repo'
config = self._create_config() config = self._create_config()
self.assertTrue(config.validate()) self.assertTrue(config.validate())

View File

@ -53,23 +53,13 @@ class BaseCi:
def __init__(self, config): def __init__(self, config):
self.config = config self.config = config
self.workspace = workspace_utils.Workspace(config) self.workspace = workspace_utils.Workspace(config)
self._repo_dir = None
@property
def repo_dir(self): def repo_dir(self):
"""Returns the source repo path, if it has been checked out. None is """Returns the source repo path, if it has been checked out. None is
returned otherwise.""" returned otherwise."""
if not os.path.exists(self.workspace.repo_storage): raise NotImplementedError('Child class must implement method.')
return None
# Note: this assumes there is only one repo checked out here.
listing = os.listdir(self.workspace.repo_storage)
if len(listing) != 1:
raise RuntimeError('Invalid repo storage.')
repo_path = os.path.join(self.workspace.repo_storage, listing[0])
if not os.path.isdir(repo_path):
raise RuntimeError('Repo is not a directory.')
return repo_path
def prepare_for_fuzzer_build(self): def prepare_for_fuzzer_build(self):
"""Builds the fuzzer builder image and gets the source code we need to """Builds the fuzzer builder image and gets the source code we need to
@ -152,6 +142,31 @@ def checkout_specified_commit(repo_manager_obj, pr_ref, commit_sha):
class GithubCiMixin: class GithubCiMixin:
"""Mixin for Github based CI systems.""" """Mixin for Github based CI systems."""
def __init__(self, config):
super().__init__(config)
# Unlike in other classes, here _repo_dir is the parent directory of the
# repo, not its actual directory.
self._repo_dir = self.workspace.repo_storage
@property
def repo_dir(self):
"""Returns the source repo path, if it has been checked out. None is
returned otherwise."""
if not os.path.exists(self._repo_dir):
logging.warning('Repo dir: %s does not exist.', self._repo_dir)
return None
# Note: this assumes there is only one repo checked out here.
listing = os.listdir(self._repo_dir)
if len(listing) != 1:
raise RuntimeError('Invalid repo directory.')
repo_path = os.path.join(self._repo_dir, listing[0])
if not os.path.isdir(repo_path):
raise RuntimeError('Repo is not a directory.')
return repo_path
def get_diff_base(self): def get_diff_base(self):
"""Returns the base to diff against with git to get the change under """Returns the base to diff against with git to get the change under
test.""" test."""
@ -217,6 +232,16 @@ class InternalGeneric(BaseCi):
"""Class representing CI for an OSS-Fuzz project on a CI other than Github """Class representing CI for an OSS-Fuzz project on a CI other than Github
actions.""" actions."""
def __init__(self, config):
super().__init__(config)
self._repo_dir = config.project_src_path
@property
def repo_dir(self):
"""Returns the source repo path, if it has been checked out. None is
returned otherwise."""
return self._repo_dir
def prepare_for_fuzzer_build(self): def prepare_for_fuzzer_build(self):
"""Builds the project builder image for an OSS-Fuzz project outside of """Builds the project builder image for an OSS-Fuzz project outside of
GitHub actions. Returns the repo_manager. Does not checkout source code GitHub actions. Returns the repo_manager. Does not checkout source code
@ -263,6 +288,16 @@ def build_external_project_docker_image(project_src, build_integration_path):
class ExternalGeneric(BaseCi): class ExternalGeneric(BaseCi):
"""CI implementation for generic CI for external (non-OSS-Fuzz) projects.""" """CI implementation for generic CI for external (non-OSS-Fuzz) projects."""
def __init__(self, config):
super().__init__(config)
self._repo_dir = config.project_src_path
@property
def repo_dir(self):
"""Returns the source repo path, if it has been checked out. None is
returned otherwise."""
return self._repo_dir
def get_diff_base(self): def get_diff_base(self):
return 'origin...' return 'origin...'

View File

@ -49,6 +49,6 @@ class BaseFilestore:
"""Downloads the build with |name| to |dst_directory|.""" """Downloads the build with |name| to |dst_directory|."""
raise NotImplementedError('Child class must implement method.') raise NotImplementedError('Child class must implement method.')
def download_coverage(self, dst_directory): def download_coverage(self, name, dst_directory):
"""Downloads the latest project coverage report.""" """Downloads the latest project coverage report."""
raise NotImplementedError('Child class must implement method.') raise NotImplementedError('Child class must implement method.')

View File

@ -21,8 +21,9 @@ import tempfile
# pylint: disable=wrong-import-position,import-error # pylint: disable=wrong-import-position,import-error
sys.path.append( sys.path.append(
os.path.join(os.path.pardir, os.path.pardir, os.path.pardir, os.path.abspath(
os.path.dirname(os.path.abspath(__file__)))) os.path.join(os.path.dirname(__file__), os.path.pardir, os.path.pardir,
os.path.pardir)))
import utils import utils
import http_utils import http_utils

View File

@ -24,8 +24,7 @@ from pyfakefs import fake_filesystem_unittest
# pylint: disable=wrong-import-position # pylint: disable=wrong-import-position
INFRA_DIR = os.path.dirname( INFRA_DIR = os.path.dirname(
os.path.dirname(os.path.dirname(os.path.dirname( os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
os.path.abspath(__file__)))))
sys.path.append(INFRA_DIR) sys.path.append(INFRA_DIR)
from filestore import github_actions from filestore import github_actions

View File

@ -12,8 +12,16 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
"""Tests for github_api.""" """Tests for github_api."""
import os
import sys
import unittest import unittest
# pylint: disable=wrong-import-position,import-error
sys.path.append(
os.path.abspath(
os.path.join(os.path.dirname(__file__), os.path.pardir, os.path.pardir,
os.path.pardir)))
from filestore.github_actions import github_api from filestore.github_actions import github_api
import test_helpers import test_helpers

View File

@ -0,0 +1,106 @@
# 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.
"""Filestore implementation using gsutil."""
import logging
import os
import posixpath
import subprocess
import sys
# pylint: disable=wrong-import-position,import-error
sys.path.append(
os.path.join(os.path.dirname(os.path.abspath(__file__)), os.pardir,
os.pardir, os.pardir))
import filestore
import utils
def _gsutil_execute(*args, parallel=True):
"""Executes a gsutil command, passing |*args| to gsutil and returns the
stdout, stderr and returncode. Exceptions on failure."""
command = ['gsutil']
if parallel:
command.append('-m')
command += list(args)
logging.info('Executing gsutil command: %s', command)
return utils.execute(command, check_result=True)
def _rsync(src, dst, delete=False):
"""Executes gsutil rsync on |src| and |dst|"""
args = ['rsync', src, dst]
if delete:
args.append('--delete')
return _gsutil_execute(*args)
class GSUtilFilestore(filestore.BaseFilestore):
"""Filestore implementation using gsutil."""
BUILD_DIR = 'build'
CRASHES_DIR = 'crashes'
CORPUS_DIR = 'corpus'
COVERAGE_DIR = 'coverage'
def __init__(self, config):
super().__init__(config)
self._cloud_bucket = self.config.cloud_bucket
def _get_gsutil_url(self, name, prefix_dir):
"""Returns the gsutil URL for |name| and |prefix_dir|."""
if not prefix_dir:
return posixpath.join(self._cloud_bucket, name)
return posixpath.join(self._cloud_bucket, prefix_dir, name)
def _upload_directory(self, name, directory, prefix, delete=False):
gsutil_url = self._get_gsutil_url(name, prefix)
return _rsync(directory, gsutil_url, delete=delete)
def _download_directory(self, name, dst_directory, prefix):
gsutil_url = self._get_gsutil_url(name, prefix)
return _rsync(gsutil_url, dst_directory)
def upload_crashes(self, name, directory):
"""Uploads the crashes at |directory| to |name|."""
# Name is going to be "current". I don't know if this makes sense outside of
# GitHub Actions.
gsutil_url = self._get_gsutil_url(name, self.CRASHES_DIR)
logging.info('Uploading crashes to %s.')
return _rsync(directory, gsutil_url)
def upload_corpus(self, name, directory, replace=False):
"""Uploads the crashes at |directory| to |name|."""
return self._upload_directory(name,
directory,
self.CORPUS_DIR,
delete=replace)
def upload_build(self, name, directory):
"""Uploads the build located at |directory| to |name|."""
return self._upload_directory(name, directory, self.BUILD_DIR)
def upload_coverage(self, name, directory):
"""Uploads the coverage report at |directory| to |name|."""
return self._upload_directory(name, directory, self.COVERAGE_DIR)
def download_corpus(self, name, dst_directory):
"""Downloads the corpus located at |name| to |dst_directory|."""
return self._download_directory(name, dst_directory, self.CORPUS_DIR)
def download_build(self, name, dst_directory):
"""Downloads the build with |name| to |dst_directory|."""
return self._download_directory(name, dst_directory, self.BUILD_DIR)
def download_coverage(self, name, dst_directory):
"""Downloads the latest project coverage report."""
return self._download_directory(name, dst_directory, self.COVERAGE_DIR)

View File

@ -0,0 +1,51 @@
# 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.
"""Empty filestore implementation for platforms that haven't implemented it."""
import logging
import filestore
# pylint:disable=no-self-use,unused-argument
class NoFilestore(filestore.BaseFilestore):
"""Empty Filestore implementation."""
def upload_crashes(self, name, directory):
"""Noop implementation of upload_crashes."""
logging.info('Not uploading crashes because no Filestore.')
def upload_corpus(self, name, directory):
"""Noop implementation of upload_corpus."""
logging.info('Not uploading corpus because no Filestore.')
def upload_build(self, name, directory):
"""Noop implementation of upload_build."""
logging.info('Not uploading build because no Filestore.')
def upload_coverage(self, name, directory):
"""Noop implementation of upload_coverage."""
logging.info('Not uploading coverage because no Filestore.')
def download_corpus(self, name, dst_directory):
"""Noop implementation of download_corpus."""
logging.info('Not downloading corpus because no Filestore.')
def download_build(self, name, dst_directory):
"""Noop implementation of download_build."""
logging.info('Not downloading build because no Filestore.')
def download_coverage(self, name, dst_directory):
"""Noop implementation of download_coverage."""
logging.info('Not downloading coverage because no Filestore.')

View File

@ -15,12 +15,20 @@
import filestore import filestore
import filestore.git import filestore.git
import filestore.github_actions import filestore.github_actions
import filestore.gsutil
import filestore.no_filestore
FILESTORE_MAPPING = {
'gsutil': filestore.gsutil.GSUtilFilestore,
'github-actions': filestore.github_actions.GithubActionsFilestore,
'git': filestore.git.GitFilestore,
'no_filestore': filestore.no_filestore.NoFilestore,
}
def get_filestore(config): def get_filestore(config):
"""Returns the correct filestore based on the platform in |config|. """Returns the correct filestore object based on the platform in |config|.
Raises an exception if there is no correct filestore for the platform.""" Raises an exception if there is no correct filestore for the platform."""
# TODO(metzman): Force specifying of filestore.
if config.platform == config.Platform.EXTERNAL_GITHUB: if config.platform == config.Platform.EXTERNAL_GITHUB:
ci_filestore = filestore.github_actions.GithubActionsFilestore(config) ci_filestore = filestore.github_actions.GithubActionsFilestore(config)
if not config.git_store_repo: if not config.git_store_repo:
@ -28,4 +36,7 @@ def get_filestore(config):
return filestore.git.GitFilestore(config, ci_filestore) return filestore.git.GitFilestore(config, ci_filestore)
raise filestore.FilestoreError('Filestore doesn\'t support platform.') filestore_cls = FILESTORE_MAPPING.get(config.filestore)
if filestore_cls is None:
raise filestore.FilestoreError('Filestore doesn\'t exist.')
return filestore_cls(config)

View File

@ -36,7 +36,8 @@ def docker_run(name, workspace, project_src_path):
command = [ command = [
'docker', 'run', '--name', name, '--rm', '-e', 'PROJECT_SRC_PATH', '-e', 'docker', 'run', '--name', name, '--rm', '-e', 'PROJECT_SRC_PATH', '-e',
'OSS_FUZZ_PROJECT_NAME', '-e', 'WORKSPACE', '-e', 'REPOSITORY', '-e', 'OSS_FUZZ_PROJECT_NAME', '-e', 'WORKSPACE', '-e', 'REPOSITORY', '-e',
'DRY_RUN', '-e', 'CI', '-e', 'SANITIZER', '-e', 'GIT_SHA' 'DRY_RUN', '-e', 'CI', '-e', 'SANITIZER', '-e', 'GIT_SHA', '-e',
'FILESTORE', '-e', 'NO_CLUSTERFUZZ_DEPLOYMENT'
] ]
if project_src_path: if project_src_path:
command += ['-v', f'{project_src_path}:{project_src_path}'] command += ['-v', f'{project_src_path}:{project_src_path}']

View File

@ -143,8 +143,9 @@ class BaseFuzzTargetRunner:
bug_found = True bug_found = True
if self.quit_on_bug_found: if self.quit_on_bug_found:
logging.info('Bug found. Stopping fuzzing.') logging.info('Bug found. Stopping fuzzing.')
return bug_found break
self.clusterfuzz_deployment.upload_crashes()
return bug_found return bug_found
@ -239,11 +240,6 @@ class BatchFuzzTargetRunner(BaseFuzzTargetRunner):
# because it is needed when we upload the build. # because it is needed when we upload the build.
fuzz_target_obj.free_disk_if_needed(delete_fuzz_target=False) fuzz_target_obj.free_disk_if_needed(delete_fuzz_target=False)
def run_fuzz_targets(self):
result = super().run_fuzz_targets()
self.clusterfuzz_deployment.upload_crashes()
return result
_RUN_FUZZERS_MODE_RUNNER_MAPPING = { _RUN_FUZZERS_MODE_RUNNER_MAPPING = {
'batch': BatchFuzzTargetRunner, 'batch': BatchFuzzTargetRunner,

View File

@ -218,11 +218,13 @@ class CiFuzzTargetRunnerTest(fake_filesystem_unittest.TestCase):
def setUp(self): def setUp(self):
self.setUpPyfakefs() self.setUpPyfakefs()
@mock.patch('clusterfuzz_deployment.OSSFuzz.upload_crashes')
@mock.patch('utils.get_fuzz_targets') @mock.patch('utils.get_fuzz_targets')
@mock.patch('run_fuzzers.CiFuzzTargetRunner.run_fuzz_target') @mock.patch('run_fuzzers.CiFuzzTargetRunner.run_fuzz_target')
@mock.patch('run_fuzzers.CiFuzzTargetRunner.create_fuzz_target_obj') @mock.patch('run_fuzzers.CiFuzzTargetRunner.create_fuzz_target_obj')
def test_run_fuzz_targets_quits(self, mock_create_fuzz_target_obj, def test_run_fuzz_targets_quits(self, mock_create_fuzz_target_obj,
mock_run_fuzz_target, mock_get_fuzz_targets): mock_run_fuzz_target, mock_get_fuzz_targets,
mock_upload_crashes):
"""Tests that run_fuzz_targets quits on the first crash it finds.""" """Tests that run_fuzz_targets quits on the first crash it finds."""
workspace = 'workspace' workspace = 'workspace'
out_path = os.path.join(workspace, 'build-out') out_path = os.path.join(workspace, 'build-out')
@ -247,6 +249,7 @@ class CiFuzzTargetRunnerTest(fake_filesystem_unittest.TestCase):
mock_create_fuzz_target_obj.return_value = magic_mock mock_create_fuzz_target_obj.return_value = magic_mock
self.assertTrue(runner.run_fuzz_targets()) self.assertTrue(runner.run_fuzz_targets())
self.assertEqual(mock_run_fuzz_target.call_count, 1) self.assertEqual(mock_run_fuzz_target.call_count, 1)
self.assertEqual(mock_upload_crashes.call_count, 1)
class BatchFuzzTargetRunnerTest(fake_filesystem_unittest.TestCase): class BatchFuzzTargetRunnerTest(fake_filesystem_unittest.TestCase):
@ -268,12 +271,11 @@ class BatchFuzzTargetRunnerTest(fake_filesystem_unittest.TestCase):
is_github=True) is_github=True)
@mock.patch('utils.get_fuzz_targets', return_value=['target1', 'target2']) @mock.patch('utils.get_fuzz_targets', return_value=['target1', 'target2'])
@mock.patch('clusterfuzz_deployment.ClusterFuzzLite.upload_build', @mock.patch('clusterfuzz_deployment.ClusterFuzzLite.upload_crashes')
return_value=True)
@mock.patch('run_fuzzers.BatchFuzzTargetRunner.run_fuzz_target') @mock.patch('run_fuzzers.BatchFuzzTargetRunner.run_fuzz_target')
@mock.patch('run_fuzzers.BatchFuzzTargetRunner.create_fuzz_target_obj') @mock.patch('run_fuzzers.BatchFuzzTargetRunner.create_fuzz_target_obj')
def test_run_fuzz_targets_quits(self, mock_create_fuzz_target_obj, def test_run_fuzz_targets_quits(self, mock_create_fuzz_target_obj,
mock_run_fuzz_target, _, __): mock_run_fuzz_target, mock_upload_crashes, _):
"""Tests that run_fuzz_targets doesn't quit on the first crash it finds.""" """Tests that run_fuzz_targets doesn't quit on the first crash it finds."""
runner = run_fuzzers.BatchFuzzTargetRunner(self.config) runner = run_fuzzers.BatchFuzzTargetRunner(self.config)
runner.initialize() runner.initialize()
@ -298,18 +300,6 @@ class BatchFuzzTargetRunnerTest(fake_filesystem_unittest.TestCase):
mock_create_fuzz_target_obj.return_value = magic_mock mock_create_fuzz_target_obj.return_value = magic_mock
self.assertTrue(runner.run_fuzz_targets()) self.assertTrue(runner.run_fuzz_targets())
self.assertEqual(mock_run_fuzz_target.call_count, 2) self.assertEqual(mock_run_fuzz_target.call_count, 2)
@mock.patch('run_fuzzers.BaseFuzzTargetRunner.run_fuzz_targets',
return_value=False)
@mock.patch('clusterfuzz_deployment.ClusterFuzzLite.upload_crashes')
def test_run_fuzz_targets_upload_crashes_and_builds(self, mock_upload_crashes,
_):
"""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()
self.assertFalse(runner.run_fuzz_targets())
self.assertEqual(mock_upload_crashes.call_count, 1) self.assertEqual(mock_upload_crashes.call_count, 1)