diff options
author | jonathanmetzman <31354670+jonathanmetzman@users.noreply.github.com> | 2020-12-07 10:50:11 -0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2020-12-07 10:50:11 -0800 |
commit | b0b99d5ccdf5e2e49cfe3138fbcf64e6fef6ea7f (patch) | |
tree | 4afa704c82330c785a468ecfe5097314e7b06365 /infra/cifuzz | |
parent | a24cebec02a0c97247bef31963d5f5fadbaa4ebf (diff) | |
download | oss-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.yml | 8 | ||||
-rw-r--r-- | infra/cifuzz/actions/build_fuzzers/build_fuzzers_entrypoint.py | 96 | ||||
-rw-r--r-- | infra/cifuzz/cifuzz-base/Dockerfile | 2 | ||||
-rw-r--r-- | infra/cifuzz/cifuzz.py | 387 | ||||
-rw-r--r-- | infra/cifuzz/cifuzz_test.py | 29 | ||||
-rw-r--r-- | infra/cifuzz/fuzz_target_test.py | 8 | ||||
-rw-r--r-- | infra/cifuzz/test_files/external-project/Makefile | 44 | ||||
-rw-r--r-- | infra/cifuzz/test_files/external-project/do_stuff_fuzzer.cpp | 24 | ||||
-rw-r--r-- | infra/cifuzz/test_files/external-project/do_stuff_fuzzer.dict | 6 | ||||
-rw-r--r-- | infra/cifuzz/test_files/external-project/my_api.cpp | 36 | ||||
-rw-r--r-- | infra/cifuzz/test_files/external-project/my_api.h | 19 | ||||
-rw-r--r-- | infra/cifuzz/test_files/external-project/oss-fuzz/Dockerfile | 22 | ||||
-rw-r--r-- | infra/cifuzz/test_files/external-project/oss-fuzz/build.sh | 24 | ||||
-rw-r--r-- | infra/cifuzz/test_files/external-project/standalone_fuzz_target_runner.cpp | 47 |
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; +} |