diff options
author | jonathanmetzman <31354670+jonathanmetzman@users.noreply.github.com> | 2021-03-12 07:27:07 -0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-03-12 07:27:07 -0800 |
commit | 3465403f3066d41641109aab7d2ab52b5b8ea603 (patch) | |
tree | b9f86191eeed5d4a9bafd389d201495c5b7308e9 /infra/cifuzz | |
parent | 5a00fd347ee44eec4737c322619a2c39c084db15 (diff) | |
download | oss-fuzz-3465403f3066d41641109aab7d2ab52b5b8ea603.tar.gz |
[CIFuzz] Add functionality to save diskspace (#5342)
* [CIFuzz] Add functionality to save diskspace.
Add a LOW_DISK_SPACE env/config var. When this is specified
(always true for Github actions) run_fuzzers will delete
base-builder and the project builder image before fuzzing.
After it finishes fuzzing with a target, it will also
delete the targets, its seed corpus and its corpus.
Related: #4879
Diffstat (limited to 'infra/cifuzz')
-rw-r--r-- | infra/cifuzz/actions/build_fuzzers/action.yml | 1 | ||||
-rw-r--r-- | infra/cifuzz/actions/run_fuzzers/action.yml | 1 | ||||
-rw-r--r-- | infra/cifuzz/build_fuzzers.py | 12 | ||||
-rw-r--r-- | infra/cifuzz/config_utils.py | 10 | ||||
-rw-r--r-- | infra/cifuzz/docker.py | 38 | ||||
-rw-r--r-- | infra/cifuzz/environment.py | 54 | ||||
-rw-r--r-- | infra/cifuzz/fuzz_target.py | 37 | ||||
-rw-r--r-- | infra/cifuzz/run_fuzzers.py | 4 | ||||
-rw-r--r-- | infra/cifuzz/run_fuzzers_entrypoint.py | 19 |
9 files changed, 158 insertions, 18 deletions
diff --git a/infra/cifuzz/actions/build_fuzzers/action.yml b/infra/cifuzz/actions/build_fuzzers/action.yml index 35ff010b3..a8a81ac02 100644 --- a/infra/cifuzz/actions/build_fuzzers/action.yml +++ b/infra/cifuzz/actions/build_fuzzers/action.yml @@ -35,3 +35,4 @@ runs: SANITIZER: ${{ inputs.sanitizer }} PROJECT_SRC_PATH: ${{ inputs.project-src-path }} BUILD_INTEGRATION_PATH: ${{ inputs.build-integration-path }} + LOW_DISK_SPACE: 'True' diff --git a/infra/cifuzz/actions/run_fuzzers/action.yml b/infra/cifuzz/actions/run_fuzzers/action.yml index aec899608..d1c03c833 100644 --- a/infra/cifuzz/actions/run_fuzzers/action.yml +++ b/infra/cifuzz/actions/run_fuzzers/action.yml @@ -44,3 +44,4 @@ runs: # for running because we use it to distinguish OSS-Fuzz from non-OSS-Fuzz. # We should do something explicit instead. BUILD_INTEGRATION_PATH: ${{ inputs.build-integration-path }} + LOW_DISK_SPACE: 'True' diff --git a/infra/cifuzz/build_fuzzers.py b/infra/cifuzz/build_fuzzers.py index 2dae6c986..78180b52b 100644 --- a/infra/cifuzz/build_fuzzers.py +++ b/infra/cifuzz/build_fuzzers.py @@ -20,6 +20,7 @@ import sys import affected_fuzz_targets import continuous_integration +import docker # pylint: disable=wrong-import-position,import-error sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) @@ -94,7 +95,7 @@ class Builder: # pylint: disable=too-many-instance-attributes self.handle_msan_prebuild(container) docker_args.extend([ - 'gcr.io/oss-fuzz/' + self.config.project_name, + docker.get_project_image_name(self.config.project_name), '/bin/bash', '-c', ]) @@ -119,8 +120,7 @@ class Builder: # pylint: disable=too-many-instance-attributes helper.docker_run([ '--volumes-from', container, '-e', 'WORK={work_dir}'.format(work_dir=self.work_dir), - 'gcr.io/oss-fuzz-base/base-sanitizer-libs-builder', 'patch_build.py', - '/out' + docker.MSAN_LIBS_BUILDER_TAG, 'patch_build.py', '/out' ]) def handle_msan_prebuild(self, container): @@ -128,8 +128,8 @@ class Builder: # pylint: disable=too-many-instance-attributes returns docker arguments to use that directory for MSAN libs.""" logging.info('Copying MSAN libs.') helper.docker_run([ - '--volumes-from', container, 'gcr.io/oss-fuzz-base/msan-libs-builder', - 'bash', '-c', 'cp -r /msan {work_dir}'.format(work_dir=self.work_dir) + '--volumes-from', container, docker.MSAN_LIBS_BUILDER_TAG, 'bash', '-c', + 'cp -r /msan {work_dir}'.format(work_dir=self.work_dir) ]) def build(self): @@ -238,7 +238,7 @@ def check_fuzzer_build(out_dir, command += ['-e', 'OUT=' + out_dir, '--volumes-from', container] else: command += ['-v', '%s:/out' % out_dir] - command.extend(['-t', 'gcr.io/oss-fuzz-base/base-runner', 'test_all.py']) + command.extend(['-t', docker.BASE_RUNNER_TAG, 'test_all.py']) exit_code = helper.docker_run(command) logging.info('check fuzzer build exit code: %d', exit_code) if exit_code: diff --git a/infra/cifuzz/config_utils.py b/infra/cifuzz/config_utils.py index 7ee3444b3..9cf018bfd 100644 --- a/infra/cifuzz/config_utils.py +++ b/infra/cifuzz/config_utils.py @@ -18,14 +18,16 @@ import enum import os import json +import environment + def _get_project_repo_name(): - return os.path.basename(os.getenv('GITHUB_REPOSITORY', '')) + return os.path.basename(environment.get('GITHUB_REPOSITORY', '')) def _get_pr_ref(event): if event == 'pull_request': - return os.getenv('GITHUB_REF') + return environment.get('GITHUB_REF') return None @@ -40,7 +42,7 @@ def _get_project_name(): def _is_dry_run(): """Returns True if configured to do a dry run.""" - return os.getenv('DRY_RUN', 'false').lower() == 'true' + return environment.get_bool('DRY_RUN', 'false') def get_project_src_path(workspace): @@ -98,6 +100,8 @@ class BaseConfig: event_path = os.getenv('GITHUB_EVENT_PATH') self.is_github = bool(event_path) logging.debug('Is github: %s.', self.is_github) + # TODO(metzman): Parse env like we do in ClusterFuzz. + self.low_disk_space = environment.get('LOW_DISK_SPACE', False) @property def is_internal(self): diff --git a/infra/cifuzz/docker.py b/infra/cifuzz/docker.py new file mode 100644 index 000000000..eb993e28d --- /dev/null +++ b/infra/cifuzz/docker.py @@ -0,0 +1,38 @@ +# 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. +"""Module for dealing with docker.""" +import os +import sys + +# pylint: disable=wrong-import-position,import-error +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +import utils + +BASE_BUILDER_TAG = 'gcr.io/oss-fuzz-base/base-builder' +BASE_RUNNER_TAG = 'gcr.io/oss-fuzz-base/base-runner' +MSAN_LIBS_BUILDER_TAG = 'gcr.io/oss-fuzz-base/msan-libs-builder' +PROJECT_TAG_PREFIX = 'gcr.io/oss-fuzz/' + + +def get_project_image_name(project): + """Returns the name of the project builder image for |project_name|.""" + return PROJECT_TAG_PREFIX + project + + +def delete_images(images): + """Deletes |images|.""" + command = ['docker', 'rmi', '-f'] + images + utils.execute(command) + utils.execute(['docker', 'builder', 'prune', '-f']) diff --git a/infra/cifuzz/environment.py b/infra/cifuzz/environment.py new file mode 100644 index 000000000..4cc0f846b --- /dev/null +++ b/infra/cifuzz/environment.py @@ -0,0 +1,54 @@ +# 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. +"""Module for dealing with env vars.""" + +import ast +import os + + +def _eval_value(value_string): + """Returns evaluated value.""" + try: + return ast.literal_eval(value_string) + except: # pylint: disable=bare-except + # String fallback. + return value_string + + +def get(env_var, default_value=None): + """Returns an environment variable value.""" + value_string = os.getenv(env_var) + if value_string is None: + return default_value + + return _eval_value(value_string) + + +def get_bool(env_var, default_value=None): + """Returns a boolean environment variable value. This is needed because a lot + of CIFuzz users specified 'false' for dry-run. So we need to special case + this.""" + value = get(env_var, default_value) + if not isinstance(value, str): + return bool(value) + + lower_value = value.lower() + allowed_values = {'true', 'false'} + if lower_value not in allowed_values: + raise Exception(('Bool env var {env_var} value {value} is invalid. ' + 'Must be one of {allowed_values}').format( + env_var=env_var, + value=value, + allowed_values=allowed_values)) + return lower_value == 'true' diff --git a/infra/cifuzz/fuzz_target.py b/infra/cifuzz/fuzz_target.py index 7bccfa4e1..4bdd17969 100644 --- a/infra/cifuzz/fuzz_target.py +++ b/infra/cifuzz/fuzz_target.py @@ -16,10 +16,13 @@ import collections import logging import os import re +import shutil import stat import subprocess import sys +import docker + # pylint: disable=wrong-import-position,import-error sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) import utils @@ -28,6 +31,8 @@ logging.basicConfig( format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', level=logging.DEBUG) +# Use a fixed seed for determinism. Use len_control=0 since we don't have enough +# time fuzzing for len_control to make sense (probably). LIBFUZZER_OPTIONS = '-seed=1337 -len_control=0' # The number of reproduce attempts for a crash. @@ -78,6 +83,7 @@ class FuzzTarget: self.out_dir = out_dir self.clusterfuzz_deployment = clusterfuzz_deployment self.config = config + self.latest_corpus_path = None def fuzz(self): """Starts the fuzz target run for the length of time specified by duration. @@ -98,8 +104,7 @@ class FuzzTarget: command += [ '-e', 'FUZZING_ENGINE=libfuzzer', '-e', 'SANITIZER=' + self.config.sanitizer, '-e', 'CIFUZZ=True', '-e', - 'RUN_FUZZER_MODE=interactive', 'gcr.io/oss-fuzz-base/base-runner', - 'bash', '-c' + 'RUN_FUZZER_MODE=interactive', docker.BASE_RUNNER_TAG, 'bash', '-c' ] run_fuzzer_command = 'run_fuzzer {fuzz_target} {options}'.format( @@ -107,10 +112,10 @@ class FuzzTarget: options=LIBFUZZER_OPTIONS + ' -max_total_time=' + str(self.duration)) # If corpus can be downloaded use it for fuzzing. - latest_corpus_path = self.clusterfuzz_deployment.download_corpus( + self.latest_corpus_path = self.clusterfuzz_deployment.download_corpus( self.target_name, self.out_dir) - if latest_corpus_path: - run_fuzzer_command = run_fuzzer_command + ' ' + latest_corpus_path + if self.latest_corpus_path: + run_fuzzer_command = run_fuzzer_command + ' ' + self.latest_corpus_path command.append(run_fuzzer_command) logging.info('Running command: %s', ' '.join(command)) @@ -140,6 +145,25 @@ class FuzzTarget: return FuzzResult(testcase, stderr) return FuzzResult(None, None) + def free_disk_if_needed(self): + """Deletes things that are no longer needed from fuzzing this fuzz target to + save disk space if needed.""" + if not self.config.low_disk_space: + return + logging.info( + 'Deleting corpus, seed corpus and fuzz target of %s to save disk.', + self.target_name) + + # Delete the seed corpus, corpus, and fuzz target. + if self.latest_corpus_path and os.path.exists(self.latest_corpus_path): + shutil.rmtree(self.latest_corpus_path) + + os.remove(self.target_path) + target_seed_corpus_path = self.target_path + '_seed_corpus.zip' + if os.path.exists(target_seed_corpus_path): + os.remove(target_seed_corpus_path) + logging.info('Done deleting.') + def is_reproducible(self, testcase, target_path): """Checks if the testcase reproduces. @@ -176,8 +200,7 @@ class FuzzTarget: ] command += [ - '-t', 'gcr.io/oss-fuzz-base/base-runner', 'reproduce', self.target_name, - '-runs=100' + '-t', docker.BASE_RUNNER_TAG, 'reproduce', self.target_name, '-runs=100' ] logging.info('Running reproduce command: %s.', ' '.join(command)) diff --git a/infra/cifuzz/run_fuzzers.py b/infra/cifuzz/run_fuzzers.py index de2914f19..419a5169f 100644 --- a/infra/cifuzz/run_fuzzers.py +++ b/infra/cifuzz/run_fuzzers.py @@ -91,7 +91,9 @@ class BaseFuzzTargetRunner: """Fuzzes with |fuzz_target_obj| and returns the result.""" # TODO(metzman): Make children implement this so that the batch runner can # do things differently. - return fuzz_target_obj.fuzz() + result = fuzz_target_obj.fuzz() + fuzz_target_obj.free_disk_if_needed() + return result @property def quit_on_bug_found(self): diff --git a/infra/cifuzz/run_fuzzers_entrypoint.py b/infra/cifuzz/run_fuzzers_entrypoint.py index f810e38f8..46e208dc0 100644 --- a/infra/cifuzz/run_fuzzers_entrypoint.py +++ b/infra/cifuzz/run_fuzzers_entrypoint.py @@ -11,11 +11,12 @@ # 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. -"""Runs specific OSS-Fuzz project's fuzzers for CI tools.""" +"""Runs a specific OSS-Fuzz project's fuzzers for CI tools.""" import logging import sys import config_utils +import docker import run_fuzzers # pylint: disable=c-extension-no-member @@ -26,6 +27,21 @@ logging.basicConfig( level=logging.DEBUG) +def delete_unneeded_docker_images(config): + """Deletes unneeded docker images if running in an environment with low + disk space.""" + if not config.low_disk_space: + return + logging.info('Deleting builder docker images to save disk space.') + project_image = docker.get_project_image_name(config.project_name) + images = [ + project_image, + docker.BASE_RUNNER_TAG, + docker.MSAN_LIBS_BUILDER_TAG, + ] + docker.delete_images(images) + + def main(): """Runs OSS-Fuzz project's fuzzers for CI tools. This is the entrypoint for the run_fuzzers github action. @@ -62,6 +78,7 @@ def main(): logging.error('This script needs to be run within Github actions.') return returncode + delete_unneeded_docker_images(config) # Run the specified project's fuzzers from the build. result = run_fuzzers.run_fuzzers(config) if result == run_fuzzers.RunFuzzersResult.ERROR: |