aboutsummaryrefslogtreecommitdiff
path: root/infra/cifuzz
diff options
context:
space:
mode:
authorjonathanmetzman <31354670+jonathanmetzman@users.noreply.github.com>2020-12-07 10:50:11 -0800
committerGitHub <noreply@github.com>2020-12-07 10:50:11 -0800
commitb0b99d5ccdf5e2e49cfe3138fbcf64e6fef6ea7f (patch)
tree4afa704c82330c785a468ecfe5097314e7b06365 /infra/cifuzz
parenta24cebec02a0c97247bef31963d5f5fadbaa4ebf (diff)
downloadoss-fuzz-b0b99d5ccdf5e2e49cfe3138fbcf64e6fef6ea7f.tar.gz
Cifuzz external build (#4656)
* Support building fuzzers for projects outside of OSS-Fuzz * Use retry wrapper * Fix some tests.
Diffstat (limited to 'infra/cifuzz')
-rw-r--r--infra/cifuzz/actions/build_fuzzers/action.yml8
-rw-r--r--infra/cifuzz/actions/build_fuzzers/build_fuzzers_entrypoint.py96
-rw-r--r--infra/cifuzz/cifuzz-base/Dockerfile2
-rw-r--r--infra/cifuzz/cifuzz.py387
-rw-r--r--infra/cifuzz/cifuzz_test.py29
-rw-r--r--infra/cifuzz/fuzz_target_test.py8
-rw-r--r--infra/cifuzz/test_files/external-project/Makefile44
-rw-r--r--infra/cifuzz/test_files/external-project/do_stuff_fuzzer.cpp24
-rw-r--r--infra/cifuzz/test_files/external-project/do_stuff_fuzzer.dict6
-rw-r--r--infra/cifuzz/test_files/external-project/my_api.cpp36
-rw-r--r--infra/cifuzz/test_files/external-project/my_api.h19
-rw-r--r--infra/cifuzz/test_files/external-project/oss-fuzz/Dockerfile22
-rw-r--r--infra/cifuzz/test_files/external-project/oss-fuzz/build.sh24
-rw-r--r--infra/cifuzz/test_files/external-project/standalone_fuzz_target_runner.cpp47
14 files changed, 604 insertions, 148 deletions
diff --git a/infra/cifuzz/actions/build_fuzzers/action.yml b/infra/cifuzz/actions/build_fuzzers/action.yml
index 20420eb37..2919db40e 100644
--- a/infra/cifuzz/actions/build_fuzzers/action.yml
+++ b/infra/cifuzz/actions/build_fuzzers/action.yml
@@ -14,6 +14,12 @@ inputs:
sanitizer:
description: 'The sanitizer to build the fuzzers with.'
default: 'address'
+ project-src-path:
+ description: "The path to the project's source code checkout."
+ required: false
+ build-integration-path:
+ description: "The path to the the project's build integration."
+ required: false
runs:
using: 'docker'
image: '../../../build_fuzzers.Dockerfile'
@@ -22,3 +28,5 @@ runs:
DRY_RUN: ${{ inputs.dry-run}}
ALLOWED_BROKEN_TARGETS_PERCENTAGE: ${{ inputs.allowed-broken-targets-percentage}}
SANITIZER: ${{ inputs.sanitizer }}
+ PROJECT_SRC_PATH: ${{ inputs.project-src-path }}
+ BUILD_INTEGRATION_PATH: ${{ inputs.build-integration-path }}
diff --git a/infra/cifuzz/actions/build_fuzzers/build_fuzzers_entrypoint.py b/infra/cifuzz/actions/build_fuzzers/build_fuzzers_entrypoint.py
index 689862c04..5d467e7b4 100644
--- a/infra/cifuzz/actions/build_fuzzers/build_fuzzers_entrypoint.py
+++ b/infra/cifuzz/actions/build_fuzzers/build_fuzzers_entrypoint.py
@@ -27,6 +27,32 @@ logging.basicConfig(
level=logging.DEBUG)
+def get_pr_ref(event_path):
+ """Returns the PR ref from |event_path|."""
+ with open(event_path, encoding='utf-8') as file_handle:
+ event = json.load(file_handle)
+ return 'refs/pull/{0}/merge'.format(event['pull_request']['number'])
+
+
+def get_project_src_path(workspace):
+ """Returns the manually checked out path of the project's source if specified
+ or None."""
+ # TODO(metzman): Get rid of MANUAL_SRC_PATH when Skia switches to
+ # project_src_path.
+ path = os.getenv('PROJECT_SRC_PATH', os.getenv('MANUAL_SRC_PATH'))
+ if not path:
+ logging.debug('No PROJECT_SRC_PATH.')
+ return path
+
+ logging.debug('PROJECT_SRC_PATH set.')
+ if os.path.isabs(path):
+ return path
+
+ # If |src| is not absolute, assume we are running in GitHub actions.
+ # TODO(metzman): Don't make this assumption.
+ return os.path.join(workspace, path)
+
+
def main():
"""Build OSS-Fuzz project's fuzzers for CI tools.
This script is used to kick off the Github Actions CI tool. It is the
@@ -51,52 +77,54 @@ def main():
Returns:
0 on success or 1 on failure.
"""
- oss_fuzz_project_name = os.environ.get('OSS_FUZZ_PROJECT_NAME')
- github_repo_name = os.path.basename(os.environ.get('GITHUB_REPOSITORY'))
- commit_sha = os.environ.get('GITHUB_SHA')
- event = os.environ.get('GITHUB_EVENT_NAME')
- workspace = os.environ.get('GITHUB_WORKSPACE')
- sanitizer = os.environ.get('SANITIZER').lower()
+ oss_fuzz_project_name = os.getenv('OSS_FUZZ_PROJECT_NAME')
+ github_repo_name = os.path.basename(os.getenv('GITHUB_REPOSITORY'))
+ commit_sha = os.getenv('GITHUB_SHA')
+ event = os.getenv('GITHUB_EVENT_NAME')
+ workspace = os.getenv('GITHUB_WORKSPACE')
+ sanitizer = os.getenv('SANITIZER').lower()
+ project_src_path = get_project_src_path(workspace)
+ build_integration_path = os.getenv('BUILD_INTEGRATION_PATH')
+ allowed_broken_targets_percentage = os.getenv(
+ 'ALLOWED_BROKEN_TARGETS_PERCENTAGE')
# Check if failures should not be reported.
- dry_run = (os.environ.get('DRY_RUN').lower() == 'true')
-
- # The default return code when an error occurs.
- returncode = 1
+ dry_run = os.getenv('DRY_RUN').lower() == 'true'
if dry_run:
# Sets the default return code on error to success.
returncode = 0
+ else:
+ # The default return code when an error occurs.
+ returncode = 1
if not workspace:
- logging.error('This script needs to be run in the Github action context.')
- return returncode
-
- if event == 'push' and not cifuzz.build_fuzzers(oss_fuzz_project_name,
- github_repo_name,
- workspace,
- commit_sha=commit_sha,
- sanitizer=sanitizer):
- logging.error('Error building fuzzers for project %s with commit %s.',
- oss_fuzz_project_name, commit_sha)
+ logging.error('This script needs to be run within Github actions.')
return returncode
if event == 'pull_request':
- event_path = os.environ.get('GITHUB_EVENT_PATH')
- with open(event_path, encoding='utf-8') as file_handle:
- event = json.load(file_handle)
- pr_ref = 'refs/pull/{0}/merge'.format(event['pull_request']['number'])
- if not cifuzz.build_fuzzers(oss_fuzz_project_name,
- github_repo_name,
- workspace,
- pr_ref=pr_ref,
- sanitizer=sanitizer):
- logging.error(
- 'Error building fuzzers for project %s with pull request %s.',
- oss_fuzz_project_name, pr_ref)
- return returncode
+ event_path = os.getenv('GITHUB_EVENT_PATH')
+ pr_ref = get_pr_ref(event_path)
+ else:
+ pr_ref = None
+
+ if not cifuzz.build_fuzzers(oss_fuzz_project_name,
+ github_repo_name,
+ workspace,
+ commit_sha=commit_sha,
+ pr_ref=pr_ref,
+ sanitizer=sanitizer,
+ project_src_path=project_src_path,
+ build_integration_path=build_integration_path):
+ logging.error(
+ 'Error building fuzzers for project %s (commit: %s, pr_ref: %s).',
+ oss_fuzz_project_name, commit_sha, pr_ref)
+ return returncode
out_dir = os.path.join(workspace, 'out')
- if cifuzz.check_fuzzer_build(out_dir, sanitizer=sanitizer):
+ if cifuzz.check_fuzzer_build(
+ out_dir,
+ sanitizer=sanitizer,
+ allowed_broken_targets_percentage=allowed_broken_targets_percentage):
returncode = 0
return returncode
diff --git a/infra/cifuzz/cifuzz-base/Dockerfile b/infra/cifuzz/cifuzz-base/Dockerfile
index e006c2b48..0aee3b2cf 100644
--- a/infra/cifuzz/cifuzz-base/Dockerfile
+++ b/infra/cifuzz/cifuzz-base/Dockerfile
@@ -35,4 +35,4 @@ RUN apt-get update && apt-get install docker-ce docker-ce-cli containerd.io -y
ENV OSS_FUZZ_ROOT=/opt/oss-fuzz
ADD . ${OSS_FUZZ_ROOT}
-RUN rm -rf ${OSS_FUZZ_ROOT}/infra
+RUN rm -rf ${OSS_FUZZ_ROOT}/infra \ No newline at end of file
diff --git a/infra/cifuzz/cifuzz.py b/infra/cifuzz/cifuzz.py
index 479a4e07c..69e92a4c0 100644
--- a/infra/cifuzz/cifuzz.py
+++ b/infra/cifuzz/cifuzz.py
@@ -34,6 +34,7 @@ sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import build_specified_commit
import helper
import repo_manager
+import retry
import utils
# From clusterfuzz: src/python/crash_analysis/crash_analyzer.py
@@ -77,28 +78,273 @@ logging.basicConfig(
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
level=logging.DEBUG)
+_IMAGE_BUILD_TRIES = 3
+_IMAGE_BUILD_BACKOFF = 2
-def checkout_specified_commit(build_repo_manager, pr_ref, commit_sha):
+
+def checkout_specified_commit(repo_manager_obj, pr_ref, commit_sha):
"""Checks out the specified commit or pull request using
- build_repo_manager."""
+ |repo_manager_obj|."""
try:
if pr_ref:
- build_repo_manager.checkout_pr(pr_ref)
+ repo_manager_obj.checkout_pr(pr_ref)
else:
- build_repo_manager.checkout_commit(commit_sha)
+ repo_manager_obj.checkout_commit(commit_sha)
except (RuntimeError, ValueError):
logging.error(
'Can not check out requested state %s. '
'Using current repo state', pr_ref or commit_sha)
+@retry.wrap(_IMAGE_BUILD_TRIES, _IMAGE_BUILD_BACKOFF)
+def build_external_project_docker_image(project_name, project_src,
+ build_integration_path):
+ """Builds the project builder image for an external (non-OSS-Fuzz) project.
+ Returns True on success."""
+ dockerfile_path = os.path.join(build_integration_path, 'Dockerfile')
+ tag = 'gcr.io/oss-fuzz/{project_name}'.format(project_name=project_name)
+ command = ['-t', tag, '-f', dockerfile_path, project_src]
+ return helper.docker_build(command)
+
+
+def fix_git_repo_for_diff(repo_dir):
+ """Fixes git repos cloned by the "checkout" action so that diffing works on
+ them."""
+ command = [
+ 'git', 'symbolic-ref', 'refs/remotes/origin/HEAD',
+ 'refs/remotes/origin/master'
+ ]
+ return utils.execute(command, location=repo_dir)
+
+
+def check_project_src_path(project_src_path):
+ """Returns True if |project_src_path| exists."""
+ if not os.path.exists(project_src_path):
+ logging.error(
+ 'PROJECT_SRC_PATH: %s does not exist. '
+ 'Are you mounting it correctly?', project_src_path)
+ return False
+ return True
+
+
+# pylint: disable=too-many-arguments
+
+
+class BaseBuilder: # pylint: disable=too-many-instance-attributes
+ """Base class for fuzzer builders."""
+
+ def __init__(self,
+ project_name,
+ project_repo_name,
+ workspace,
+ sanitizer,
+ host_repo_path=None):
+ self.project_name = project_name
+ self.project_repo_name = project_repo_name
+ self.workspace = workspace
+ self.out_dir = os.path.join(workspace, 'out')
+ os.makedirs(self.out_dir, exist_ok=True)
+ self.sanitizer = sanitizer
+ self.host_repo_path = host_repo_path
+ self.image_repo_path = None
+ self.repo_manager = None
+
+ def build_image_and_checkout_src(self):
+ """Builds the project builder image and checkout source code for the patch
+ we want to fuzz (if necessary). Returns True on success.
+ Must be implemented by child classes."""
+ raise NotImplementedError('Child class must implement method')
+
+ def build_fuzzers(self):
+ """Moves the source code we want to fuzz into the project builder and builds
+ the fuzzers from that source code. Returns True on success."""
+ image_src_path = os.path.dirname(self.image_repo_path)
+ command = get_common_docker_args(self.sanitizer)
+ container = utils.get_container_name()
+
+ if container:
+ command.extend(['-e', 'OUT=' + self.out_dir, '--volumes-from', container])
+ rm_path = os.path.join(self.image_repo_path, '*')
+
+ bash_command = 'rm -rf {0} && cp -r {1} {2} && compile'.format(
+ rm_path, self.host_repo_path, image_src_path)
+ else:
+ # TODO(metzman): Figure out if we can eliminate this branch.
+ command.extend([
+ '-e', 'OUT=' + '/out', '-v',
+ '%s:%s' % (self.host_repo_path, self.image_repo_path), '-v',
+ '%s:%s' % (self.out_dir, '/out')
+ ])
+ bash_command = 'compile'
+
+ command.extend([
+ 'gcr.io/oss-fuzz/' + self.project_name,
+ '/bin/bash',
+ '-c',
+ ])
+ command.append(bash_command)
+ logging.info('Building with %s sanitizer.', self.sanitizer)
+ if helper.docker_run(command):
+ # docker_run returns nonzero on failure.
+ logging.error('Building fuzzers failed.')
+ return False
+ return True
+
+ def build(self):
+ """Builds the image, checkouts the source (if needed), builds the fuzzers
+ and then removes the unaffectted fuzzers. Returns True on success."""
+ methods = [
+ self.build_image_and_checkout_src, self.build_fuzzers,
+ self.remove_unaffected_fuzzers
+ ]
+ for method in methods:
+ if not method():
+ return False
+ return True
+
+ def remove_unaffected_fuzzers(self):
+ """Removes the fuzzers unaffected by the patch."""
+ fix_git_repo_for_diff(self.host_repo_path)
+ remove_unaffected_fuzzers(self.project_name, self.out_dir,
+ self.repo_manager.get_git_diff(),
+ self.image_repo_path)
+ return True
+
+
+class ExternalGithubBuilder(BaseBuilder):
+ """Class for building non-OSS-Fuzz projects on GitHub Actions."""
+
+ def __init__(self, project_name, project_repo_name, workspace, sanitizer,
+ project_src_path, build_integration_path):
+
+ super().__init__(project_name,
+ project_repo_name,
+ workspace,
+ sanitizer,
+ host_repo_path=project_src_path)
+ self.build_integration_path = os.path.join(self.host_repo_path,
+ build_integration_path)
+ logging.info('build_integration_path %s, project_src_path %s.',
+ self.build_integration_path, self.host_repo_path)
+ self.image_repo_path = os.path.join('/src', project_repo_name)
+
+ def build_image_and_checkout_src(self):
+ """Builds the project builder image for a non-OSS-Fuzz project. Sets the
+ repo manager. Does not checkout source code since external projects are
+ expected to bring their own source code to CIFuzz. Returns True on
+ success."""
+ logging.info('Building external project.')
+ if not build_external_project_docker_image(
+ self.project_name, self.host_repo_path, self.build_integration_path):
+ logging.error('Failed to build external project.')
+ return False
+ self.repo_manager = repo_manager.RepoManager(self.host_repo_path)
+ return True
+
+
+class InternalGithubBuilder(BaseBuilder):
+ """Class for building OSS-Fuzz projects on GitHub actions."""
+
+ def __init__(self, project_name, project_repo_name, workspace, sanitizer,
+ commit_sha, pr_ref):
+ # Validate inputs.
+ assert pr_ref or commit_sha
+
+ super().__init__(project_name, project_repo_name, workspace, sanitizer)
+
+ self.commit_sha = commit_sha
+ self.pr_ref = pr_ref
+
+ def build_image_and_checkout_src(self):
+ """Builds the project builder image for a non-OSS-Fuzz project. Sets the
+ repo manager and host_repo_path. Checks out source code of project with
+ patch under test. Returns True on success."""
+ logging.info('Building OSS-Fuzz project on Github Actions.')
+ # detect_main_repo builds the image as a side effect.
+ inferred_url, self.image_repo_path = (
+ build_specified_commit.detect_main_repo(
+ self.project_name, repo_name=self.project_repo_name))
+
+ if not inferred_url or not self.image_repo_path:
+ logging.error('Could not detect repo from project %s.', self.project_name)
+ return False
+
+ git_workspace = os.path.join(self.workspace, 'storage')
+ os.makedirs(git_workspace, exist_ok=True)
+
+ # Checkout project's repo in the shared volume.
+ self.repo_manager = repo_manager.clone_repo_and_get_manager(
+ inferred_url, git_workspace, repo_name=self.project_repo_name)
+
+ self.host_repo_path = self.repo_manager.repo_dir
+
+ checkout_specified_commit(self.repo_manager, self.pr_ref, self.commit_sha)
+ return True
+
+
+class InternalGenericCiBuilder(BaseBuilder):
+ """Class for building fuzzers for an OSS-Fuzz project using on a platform
+ other than GitHub actions."""
+
+ def __init__(self, project_name, project_repo_name, workspace, sanitizer,
+ project_src_path):
+ super().__init__(project_name,
+ project_repo_name,
+ workspace,
+ sanitizer,
+ host_repo_path=project_src_path)
+
+ def build_image_and_checkout_src(self):
+ """Builds the project builder image for a non-OSS-Fuzz project. Sets the
+ repo manager. Does not checkout source code since external projects are
+ expected to bring their own source code to CIFuzz. Returns True on
+ success."""
+ logging.info('Building OSS-Fuzz project.')
+ # detect_main_repo builds the image as a side effect.
+ _, self.image_repo_path = (build_specified_commit.detect_main_repo(
+ self.project_name, repo_name=self.project_repo_name))
+
+ if not self.image_repo_path:
+ logging.error('Could not detect repo from project %s.', self.project_name)
+ return False
+
+ # Checkout project's repo in the shared volume.
+ self.repo_manager = repo_manager.RepoManager(self.host_repo_path)
+ return True
+
+
+def get_builder(project_name, project_repo_name, workspace, pr_ref, commit_sha,
+ sanitizer, project_src_path, build_integration_path):
+ """Determines what kind of build is being requested using the arguments
+ provided and instantiates and returns the correct builder object."""
+ if build_integration_path and project_src_path:
+ # Non-OSS-Fuzz projects must bring their own source and their own build
+ # integration (which is relative to that source).
+ return ExternalGithubBuilder(project_name, project_repo_name, workspace,
+ sanitizer, project_src_path,
+ build_integration_path)
+
+ if project_src_path:
+ # Builds of OSS-Fuzz projects not hosted on Github must bring their own
+ # source since the checkout logic CIFuzz implements is github-specific.
+ # TODO(metzman): Consider moving Github-actions builds of OSS-Fuzz projects
+ # to this system to reduce implementation complexity.
+ return InternalGenericCiBuilder(project_name, project_repo_name, workspace,
+ sanitizer, project_src_path)
+
+ return InternalGithubBuilder(project_name, project_repo_name, workspace,
+ sanitizer, commit_sha, pr_ref)
+
+
def build_fuzzers( # pylint: disable=too-many-arguments,too-many-locals
project_name,
project_repo_name,
workspace,
pr_ref=None,
commit_sha=None,
- sanitizer='address'):
+ sanitizer='address',
+ project_src_path=None,
+ build_integration_path=None):
"""Builds all of the fuzzers for a specific OSS-Fuzz project.
Args:
@@ -113,91 +359,15 @@ def build_fuzzers( # pylint: disable=too-many-arguments,too-many-locals
Returns:
True if build succeeded or False on failure.
"""
- # Validate inputs.
- assert pr_ref or commit_sha
- if not os.path.exists(workspace):
- logging.error('Invalid workspace: %s.', workspace)
+ # Do some quick validation.
+ if project_src_path and not check_project_src_path(project_src_path):
return False
- logging.info("Using %s sanitizer.", sanitizer)
-
- out_dir = os.path.join(workspace, 'out')
- os.makedirs(out_dir, exist_ok=True)
-
- # Build Fuzzers using docker run.
- inferred_url, project_builder_repo_path = (
- build_specified_commit.detect_main_repo(project_name,
- repo_name=project_repo_name))
- if not inferred_url or not project_builder_repo_path:
- logging.error('Could not detect repo from project %s.', project_name)
- return False
- project_repo_name = os.path.basename(project_builder_repo_path)
- src_in_project_builder = os.path.dirname(project_builder_repo_path)
-
- manual_src_path = os.getenv('MANUAL_SRC_PATH')
- if manual_src_path:
- if not os.path.exists(manual_src_path):
- logging.error(
- 'MANUAL_SRC_PATH: %s does not exist. '
- 'Are you mounting it correctly?', manual_src_path)
- return False
- # This is the path taken outside of GitHub actions.
- git_workspace = os.path.dirname(manual_src_path)
- else:
- git_workspace = os.path.join(workspace, 'storage')
- os.makedirs(git_workspace, exist_ok=True)
-
- # Checkout projects repo in the shared volume.
- build_repo_manager = repo_manager.RepoManager(inferred_url,
- git_workspace,
- repo_name=project_repo_name)
-
- if not manual_src_path:
- checkout_specified_commit(build_repo_manager, pr_ref, commit_sha)
-
- command = [
- '--cap-add',
- 'SYS_PTRACE',
- '-e',
- 'FUZZING_ENGINE=' + DEFAULT_ENGINE,
- '-e',
- 'SANITIZER=' + sanitizer,
- '-e',
- 'ARCHITECTURE=' + DEFAULT_ARCHITECTURE,
- '-e',
- 'CIFUZZ=True',
- '-e',
- 'FUZZING_LANGUAGE=c++', # FIXME: Add proper support.
- ]
- container = utils.get_container_name()
- if container:
- command += ['-e', 'OUT=' + out_dir, '--volumes-from', container]
- bash_command = 'rm -rf {0} && cp -r {1} {2} && compile'.format(
- os.path.join(src_in_project_builder, project_repo_name, '*'),
- os.path.join(git_workspace, project_repo_name), src_in_project_builder)
- else:
- command += [
- '-e', 'OUT=' + '/out', '-v',
- '%s:%s' % (os.path.join(git_workspace, project_repo_name),
- os.path.join(src_in_project_builder, project_repo_name)),
- '-v',
- '%s:%s' % (out_dir, '/out')
- ]
- bash_command = 'compile'
-
- command.extend([
- 'gcr.io/oss-fuzz/' + project_name,
- '/bin/bash',
- '-c',
- ])
- command.append(bash_command)
- if helper.docker_run(command):
- logging.error('Building fuzzers failed.')
- return False
- remove_unaffected_fuzzers(project_name, out_dir,
- build_repo_manager.get_git_diff(),
- project_builder_repo_path)
- return True
+ # Get the builder and then build the fuzzers.
+ builder = get_builder(project_name, project_repo_name, workspace, pr_ref,
+ commit_sha, sanitizer, project_src_path,
+ build_integration_path)
+ return builder.build()
def run_fuzzers( # pylint: disable=too-many-arguments,too-many-locals
@@ -267,7 +437,27 @@ def run_fuzzers( # pylint: disable=too-many-arguments,too-many-locals
return True, False
-def check_fuzzer_build(out_dir, sanitizer='address'):
+def get_common_docker_args(sanitizer):
+ """Returns a list of common docker arguments."""
+ return [
+ '--cap-add',
+ 'SYS_PTRACE',
+ '-e',
+ 'FUZZING_ENGINE=' + DEFAULT_ENGINE,
+ '-e',
+ 'SANITIZER=' + sanitizer,
+ '-e',
+ 'ARCHITECTURE=' + DEFAULT_ARCHITECTURE,
+ '-e',
+ 'CIFUZZ=True',
+ '-e',
+ 'FUZZING_LANGUAGE=c++', # FIXME: Add proper support.
+ ]
+
+
+def check_fuzzer_build(out_dir,
+ sanitizer='address',
+ allowed_broken_targets_percentage=None):
"""Checks the integrity of the built fuzzers.
Args:
@@ -284,24 +474,8 @@ def check_fuzzer_build(out_dir, sanitizer='address'):
logging.error('No fuzzers found in out directory: %s.', out_dir)
return False
- command = [
- '--cap-add',
- 'SYS_PTRACE',
- '-e',
- 'FUZZING_ENGINE=' + DEFAULT_ENGINE,
- '-e',
- 'SANITIZER=' + sanitizer,
- '-e',
- 'ARCHITECTURE=' + DEFAULT_ARCHITECTURE,
- '-e',
- 'CIFUZZ=True',
- '-e',
- 'FUZZING_LANGUAGE=c++', # FIXME: Add proper support.
- ]
+ command = get_common_docker_args(sanitizer)
- # Set ALLOWED_BROKEN_TARGETS_PERCENTAGE in docker if specified by user.
- allowed_broken_targets_percentage = os.getenv(
- 'ALLOWED_BROKEN_TARGETS_PERCENTAGE')
if allowed_broken_targets_percentage is not None:
command += [
'-e',
@@ -316,6 +490,7 @@ def check_fuzzer_build(out_dir, sanitizer='address'):
command += ['-v', '%s:/out' % out_dir]
command.extend(['-t', 'gcr.io/oss-fuzz-base/base-runner', 'test_all.py'])
exit_code = helper.docker_run(command)
+ logging.info('check fuzzer build exit code: %d', exit_code)
if exit_code:
logging.error('Check fuzzer build failed.')
return False
diff --git a/infra/cifuzz/cifuzz_test.py b/infra/cifuzz/cifuzz_test.py
index e7624f549..87ce8f236 100644
--- a/infra/cifuzz/cifuzz_test.py
+++ b/infra/cifuzz/cifuzz_test.py
@@ -33,6 +33,7 @@ OSS_FUZZ_DIR = os.path.dirname(INFRA_DIR)
import cifuzz
import fuzz_target
+import test_helpers
# NOTE: This integration test relies on
# https://github.com/google/oss-fuzz/tree/master/projects/example project.
@@ -69,7 +70,7 @@ class BuildFuzzersTest(unittest.TestCase):
@mock.patch('build_specified_commit.detect_main_repo',
return_value=('example.com', '/path'))
- @mock.patch('repo_manager.RepoManager', return_value=None)
+ @mock.patch('repo_manager._clone', return_value=None)
@mock.patch('cifuzz.checkout_specified_commit')
@mock.patch('helper.docker_run')
def test_cifuzz_env_var(self, mocked_docker_run, _, __, ___):
@@ -97,6 +98,26 @@ class BuildFuzzersTest(unittest.TestCase):
class BuildFuzzersIntegrationTest(unittest.TestCase):
"""Integration tests for build_fuzzers."""
+ def setUp(self):
+ test_helpers.patch_environ(self)
+
+ def test_external_project(self):
+ """Tests building fuzzers from an external project."""
+ project_name = 'external-project'
+ project_src_path = os.path.join(TEST_FILES_PATH, project_name)
+ build_integration_path = os.path.join(project_src_path, 'oss-fuzz')
+ with tempfile.TemporaryDirectory() as tmp_dir:
+ out_path = os.path.join(tmp_dir, 'out')
+ os.mkdir(out_path)
+ self.assertTrue(
+ cifuzz.build_fuzzers(project_name,
+ project_name,
+ tmp_dir,
+ project_src_path=project_src_path,
+ build_integration_path=build_integration_path))
+ self.assertTrue(
+ os.path.exists(os.path.join(out_path, EXAMPLE_BUILD_FUZZER)))
+
def test_valid_commit(self):
"""Tests building fuzzers with valid inputs."""
with tempfile.TemporaryDirectory() as tmp_dir:
@@ -343,14 +364,14 @@ class CheckFuzzerBuildTest(unittest.TestCase):
"""Checks a directory that exists but does not have fuzzers is False."""
self.assertFalse(cifuzz.check_fuzzer_build(TEST_FILES_PATH))
- @mock.patch.dict(os.environ, {'ALLOWED_BROKEN_TARGETS_PERCENTAGE': '0'})
@mock.patch('helper.docker_run')
def test_allow_broken_fuzz_targets_percentage(self, mocked_docker_run):
"""Tests that ALLOWED_BROKEN_TARGETS_PERCENTAGE is set when running
- docker if it is set in the environment."""
+ docker if passed to check_fuzzer_build."""
mocked_docker_run.return_value = 0
test_fuzzer_dir = os.path.join(TEST_FILES_PATH, 'out')
- cifuzz.check_fuzzer_build(test_fuzzer_dir)
+ cifuzz.check_fuzzer_build(test_fuzzer_dir,
+ allowed_broken_targets_percentage='0')
self.assertIn('-e ALLOWED_BROKEN_TARGETS_PERCENTAGE=0',
' '.join(mocked_docker_run.call_args[0][0]))
diff --git a/infra/cifuzz/fuzz_target_test.py b/infra/cifuzz/fuzz_target_test.py
index 3e8788813..8f72f32cb 100644
--- a/infra/cifuzz/fuzz_target_test.py
+++ b/infra/cifuzz/fuzz_target_test.py
@@ -320,9 +320,11 @@ class DownloadOSSFuzzBuildDirIntegrationTest(unittest.TestCase):
def test_invalid_build_dir(self):
"""Tests the download returns None when out_dir doesn't exist."""
- test_target = fuzz_target.FuzzTarget('/example/do_stuff_fuzzer', 10,
- 'not/a/dir', 'example')
- self.assertIsNone(test_target.download_oss_fuzz_build())
+ with tempfile.TemporaryDirectory() as tmp_dir:
+ invalid_dir = os.path.join(tmp_dir, 'not/a/dir')
+ test_target = fuzz_target.FuzzTarget('/example/do_stuff_fuzzer', 10,
+ invalid_dir, 'example')
+ self.assertIsNone(test_target.download_oss_fuzz_build())
class DownloadUrlTest(unittest.TestCase):
diff --git a/infra/cifuzz/test_files/external-project/Makefile b/infra/cifuzz/test_files/external-project/Makefile
new file mode 100644
index 000000000..2c1773776
--- /dev/null
+++ b/infra/cifuzz/test_files/external-project/Makefile
@@ -0,0 +1,44 @@
+# Copyright 2017 Google Inc. All Rights Reserved.
+# Licensed under the Apache License, Version 2.0 (the "License");
+
+# Simple example of a build file that nicely integrates a fuzz target
+# with the rest of the project.
+#
+# We use 'make' as the build system, but these ideas are applicable
+# to any other build system
+
+# By default, use our own standalone_fuzz_target_runner.
+# This runner does no fuzzing, but simply executes the inputs
+# provided via parameters.
+# Run e.g. "make all LIB_FUZZING_ENGINE=/path/to/libFuzzer.a"
+# to link the fuzzer(s) against a real fuzzing engine.
+#
+# OSS-Fuzz will define its own value for LIB_FUZZING_ENGINE.
+LIB_FUZZING_ENGINE ?= standalone_fuzz_target_runner.o
+
+# Values for CC, CFLAGS, CXX, CXXFLAGS are provided by OSS-Fuzz.
+# Outside of OSS-Fuzz use the ones you prefer or rely on the default values.
+# Do not use the -fsanitize=* flags by default.
+# OSS-Fuzz will use different -fsanitize=* flags for different builds (asan, ubsan, msan, ...)
+
+# You may add extra compiler flags like this:
+CXXFLAGS += -std=c++11
+
+all: do_stuff_fuzzer
+
+clean:
+ rm -fv *.a *.o *_fuzzer crash-* *.zip
+
+# Fuzz target, links against $LIB_FUZZING_ENGINE, so that
+# you may choose which fuzzing engine to use.
+do_stuff_fuzzer: do_stuff_fuzzer.cpp my_api.a standalone_fuzz_target_runner.o
+ ${CXX} ${CXXFLAGS} $< my_api.a ${LIB_FUZZING_ENGINE} -o $@
+
+
+# The library itself.
+my_api.a: my_api.cpp my_api.h
+ ${CXX} ${CXXFLAGS} $< -c
+ ar ruv my_api.a my_api.o
+
+# The standalone fuzz target runner.
+standalone_fuzz_target_runner.o: standalone_fuzz_target_runner.cpp
diff --git a/infra/cifuzz/test_files/external-project/do_stuff_fuzzer.cpp b/infra/cifuzz/test_files/external-project/do_stuff_fuzzer.cpp
new file mode 100644
index 000000000..71fa8cae2
--- /dev/null
+++ b/infra/cifuzz/test_files/external-project/do_stuff_fuzzer.cpp
@@ -0,0 +1,24 @@
+// Copyright 2020 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.
+#include "my_api.h"
+
+#include <string>
+
+// Simple fuzz target for DoStuff().
+// See http://libfuzzer.info for details.
+extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
+ std::string str(reinterpret_cast<const char *>(data), size);
+ DoStuff(str); // Disregard the output.
+ return 0;
+}
diff --git a/infra/cifuzz/test_files/external-project/do_stuff_fuzzer.dict b/infra/cifuzz/test_files/external-project/do_stuff_fuzzer.dict
new file mode 100644
index 000000000..224679bf4
--- /dev/null
+++ b/infra/cifuzz/test_files/external-project/do_stuff_fuzzer.dict
@@ -0,0 +1,6 @@
+# A dictionary for more efficient fuzzing of DoStuff().
+# If the inputs contain multi-byte tokens, list them here.
+# See http://libfuzzer.info#dictionaries
+"foo"
+"bar"
+"ouch"
diff --git a/infra/cifuzz/test_files/external-project/my_api.cpp b/infra/cifuzz/test_files/external-project/my_api.cpp
new file mode 100644
index 000000000..9a2c1bc1c
--- /dev/null
+++ b/infra/cifuzz/test_files/external-project/my_api.cpp
@@ -0,0 +1,36 @@
+// Copyright 2020 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.
+
+// Implementation of "my_api".
+#include "my_api.h"
+
+#include <vector>
+
+// Do some computations with 'str', return the result.
+// This function contains a bug. Can you spot it?
+size_t DoStuff(const std::string &str) {
+ std::vector<int> Vec({0, 1, 2, 3, 4});
+ size_t Idx = 0;
+ if (str.size() > 5)
+ Idx++;
+ if (str.find("foo") != std::string::npos)
+ Idx++;
+ if (str.find("bar") != std::string::npos)
+ Idx++;
+ if (str.find("ouch") != std::string::npos)
+ Idx++;
+ if (str.find("omg") != std::string::npos)
+ Idx++;
+ return Vec[Idx];
+}
diff --git a/infra/cifuzz/test_files/external-project/my_api.h b/infra/cifuzz/test_files/external-project/my_api.h
new file mode 100644
index 000000000..325aa15cc
--- /dev/null
+++ b/infra/cifuzz/test_files/external-project/my_api.h
@@ -0,0 +1,19 @@
+// Copyright 2020 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.
+
+// A library that does ... stuff.
+// Serves as an example of good fuzz testing and OSS-Fuzz integration.
+#include <string>
+
+size_t DoStuff(const std::string &str);
diff --git a/infra/cifuzz/test_files/external-project/oss-fuzz/Dockerfile b/infra/cifuzz/test_files/external-project/oss-fuzz/Dockerfile
new file mode 100644
index 000000000..e9dc33031
--- /dev/null
+++ b/infra/cifuzz/test_files/external-project/oss-fuzz/Dockerfile
@@ -0,0 +1,22 @@
+# Copyright 2020 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.
+#
+################################################################################
+
+FROM gcr.io/oss-fuzz-base/base-builder
+RUN apt-get update && apt-get install -y make
+
+COPY . $SRC/external-project
+WORKDIR external-project
+COPY oss-fuzz/build.sh $SRC/
diff --git a/infra/cifuzz/test_files/external-project/oss-fuzz/build.sh b/infra/cifuzz/test_files/external-project/oss-fuzz/build.sh
new file mode 100644
index 000000000..2c52ef90f
--- /dev/null
+++ b/infra/cifuzz/test_files/external-project/oss-fuzz/build.sh
@@ -0,0 +1,24 @@
+#!/bin/bash -eu
+# Copyright 2020 Google Inc.
+#
+# 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.
+#
+################################################################################
+
+make clean # Not strictly necessary, since we are building in a fresh dir.
+make -j$(nproc) all # Build the fuzz targets.
+
+# Copy the fuzzer executables, zip-ed corpora, option and dictionary files to $OUT
+find . -name '*_fuzzer' -exec cp -v '{}' $OUT ';'
+find . -name '*_fuzzer.dict' -exec cp -v '{}' $OUT ';' # If you have dictionaries.
+find . -name '*_fuzzer.options' -exec cp -v '{}' $OUT ';' # If you have custom options.
diff --git a/infra/cifuzz/test_files/external-project/standalone_fuzz_target_runner.cpp b/infra/cifuzz/test_files/external-project/standalone_fuzz_target_runner.cpp
new file mode 100644
index 000000000..38a0454f0
--- /dev/null
+++ b/infra/cifuzz/test_files/external-project/standalone_fuzz_target_runner.cpp
@@ -0,0 +1,47 @@
+// Copyright 2020 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Example of a standalone runner for "fuzz targets".
+// It reads all files passed as parameters and feeds their contents
+// one by one into the fuzz target (LLVMFuzzerTestOneInput).
+// This runner does not do any fuzzing, but allows us to run the fuzz target
+// on the test corpus (e.g. "do_stuff_test_data") or on a single file,
+// e.g. the one that comes from a bug report.
+
+#include <cassert>
+#include <iostream>
+#include <fstream>
+#include <vector>
+
+// Forward declare the "fuzz target" interface.
+// We deliberately keep this inteface simple and header-free.
+extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size);
+
+int main(int argc, char **argv) {
+ for (int i = 1; i < argc; i++) {
+ std::ifstream in(argv[i]);
+ in.seekg(0, in.end);
+ size_t length = in.tellg();
+ in.seekg (0, in.beg);
+ std::cout << "Reading " << length << " bytes from " << argv[i] << std::endl;
+ // Allocate exactly length bytes so that we reliably catch buffer overflows.
+ std::vector<char> bytes(length);
+ in.read(bytes.data(), bytes.size());
+ assert(in);
+ LLVMFuzzerTestOneInput(reinterpret_cast<const uint8_t *>(bytes.data()),
+ bytes.size());
+ std::cout << "Execution successful" << std::endl;
+ }
+ return 0;
+}