diff --git a/infra/cifuzz/actions/build_fuzzers/Dockerfile b/infra/cifuzz/actions/build_fuzzers/Dockerfile new file mode 100644 index 000000000..0e05bcde6 --- /dev/null +++ b/infra/cifuzz/actions/build_fuzzers/Dockerfile @@ -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. +# +################################################################################ +# Docker image to run CIFuzz in. + +FROM gcr.io/oss-fuzz-base/cifuzz-base + +# Copies your code file from action repository to the container +COPY build_fuzzers_entrypoint.py /opt/build_fuzzers_entrypoint.py + +# Python file to execute when the docker container starts up +ENTRYPOINT ["python3", "/opt/entrypoint.py"] diff --git a/infra/cifuzz/actions/action.yml b/infra/cifuzz/actions/build_fuzzers/action.yml similarity index 72% rename from infra/cifuzz/actions/action.yml rename to infra/cifuzz/actions/build_fuzzers/action.yml index 72df36d54..cea4c9f4f 100644 --- a/infra/cifuzz/actions/action.yml +++ b/infra/cifuzz/actions/build_fuzzers/action.yml @@ -5,10 +5,6 @@ inputs: project-name: description: 'Name of the corresponding OSS-Fuzz project.' required: true - fuzz-seconds: - description: 'The total time allotted for fuzzing in seconds.' - required: true - default: 360 dry-run: description: 'If set, run the action without actually reporting a failure.' default: false @@ -17,5 +13,4 @@ runs: image: 'Dockerfile' env: PROJECT_NAME: ${{ inputs.project-name }} - FUZZ_SECONDS: ${{ inputs.fuzz-seconds }} DRY_RUN: ${{ inputs.dry-run}} diff --git a/infra/cifuzz/actions/entrypoint.py b/infra/cifuzz/actions/build_fuzzers/build_fuzzers_entrypoint.py similarity index 69% rename from infra/cifuzz/actions/entrypoint.py rename to infra/cifuzz/actions/build_fuzzers/build_fuzzers_entrypoint.py index 7d924acff..92d795a39 100644 --- a/infra/cifuzz/actions/entrypoint.py +++ b/infra/cifuzz/actions/build_fuzzers/build_fuzzers_entrypoint.py @@ -28,24 +28,26 @@ logging.basicConfig( def main(): - """Runs OSS-Fuzz project's fuzzers for CI tools. + """Build OSS-Fuzz project's fuzzers for CI tools. This script is used to kick off the Github Actions CI tool. It is the - entrypoint of the Dockerfile in this directory. This action can be added to + entrypoint of the Dockerfile in this directory. This action can be added to any OSS-Fuzz project's workflow that uses Github. + Note: The resulting clusterfuzz binaries of this build are placed in + the directory: ${GITHUB_WORKSPACE}/out + Required environment variables: PROJECT_NAME: The name of OSS-Fuzz project. - FUZZ_TIME: The length of time in seconds that fuzzers are to be run. GITHUB_REPOSITORY: The name of the Github repo that called this script. GITHUB_SHA: The commit SHA that triggered this script. GITHUB_REF: The pull request reference that triggered this script. GITHUB_EVENT_NAME: The name of the hook event that triggered this script. + GITHUB_WORKSPACE: The shared volume directory where input artifacts are. Returns: 0 on success or 1 on Failure. """ oss_fuzz_project_name = os.environ.get('PROJECT_NAME') - fuzz_seconds = int(os.environ.get('FUZZ_SECONDS', 360)) github_repo_name = os.path.basename(os.environ.get('GITHUB_REPOSITORY')) pr_ref = os.environ.get('GITHUB_REF') commit_sha = os.environ.get('GITHUB_SHA') @@ -58,16 +60,6 @@ def main(): # The default return code when an error occurs. error_code = 1 if dry_run: - # A testcase file is required in order for CIFuzz to surface bugs. - # If the file does not exist, the action will crash attempting to upload it. - # The dry run needs this file because it is set to upload a test case both - # on successful runs and on failures. - out_dir = os.path.join(workspace, 'out') - os.makedirs(out_dir, exist_ok=True) - file_handle = open(os.path.join(out_dir, 'testcase'), 'w') - file_handle.write('No bugs detected.') - file_handle.close() - # Sets the default return code on error to success. error_code = 0 @@ -86,19 +78,6 @@ def main(): logging.error('Error building fuzzers for project %s with pull request %s.', oss_fuzz_project_name, pr_ref) return error_code - - # Run the specified project's fuzzers from the build. - run_status, bug_found = cifuzz.run_fuzzers(oss_fuzz_project_name, - fuzz_seconds, workspace) - if not run_status: - logging.error('Error occured while running fuzzers for project %s.', - oss_fuzz_project_name) - return error_code - if bug_found: - logging.info('Bug found.') - if not dry_run: - # Return 2 when a bug was found by a fuzzer causing the CI to fail. - return 2 return 0 diff --git a/infra/cifuzz/actions/run_fuzzers/Dockerfile b/infra/cifuzz/actions/run_fuzzers/Dockerfile new file mode 100644 index 000000000..3a5fb8b07 --- /dev/null +++ b/infra/cifuzz/actions/run_fuzzers/Dockerfile @@ -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. +# +################################################################################ +# Docker image to run CIFuzz run fuzzers action in. + +FROM gcr.io/oss-fuzz-base/cifuzz-base + +# Copies your code file from action repository to the container +COPY run_fuzzers_entrypoint.py /opt/run_fuzzers_entrypoint.py + +# Python file to execute when the docker container starts up +ENTRYPOINT ["python3", "/opt/run_fuzzers_entrypoint.py"] diff --git a/infra/cifuzz/actions/run_fuzzers/action.yml b/infra/cifuzz/actions/run_fuzzers/action.yml new file mode 100644 index 000000000..c4ce0e494 --- /dev/null +++ b/infra/cifuzz/actions/run_fuzzers/action.yml @@ -0,0 +1,17 @@ +# action.yml +name: 'run-fuzzers' +description: 'Runs fuzz target binaries for a specified length of time.' +inputs: + fuzz-seconds: + description: 'The total time allotted for fuzzing in seconds.' + required: true + default: 600 + dry-run: + description: 'If set, run the action without actually reporting a failure.' + default: false +runs: + using: 'docker' + image: 'Dockerfile' + env: + FUZZ_SECONDS: ${{ inputs.fuzz-seconds }} + DRY_RUN: ${{ inputs.dry-run}} diff --git a/infra/cifuzz/actions/run_fuzzers/run_fuzzers_entrypoint.py b/infra/cifuzz/actions/run_fuzzers/run_fuzzers_entrypoint.py new file mode 100644 index 000000000..e2bfc5ca4 --- /dev/null +++ b/infra/cifuzz/actions/run_fuzzers/run_fuzzers_entrypoint.py @@ -0,0 +1,92 @@ +# 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. +"""Runs specific OSS-Fuzz project's fuzzers for CI tools.""" +import logging +import os +import sys + +# pylint: disable=wrong-import-position +# pylint: disable=import-error +sys.path.append(os.path.join(os.environ['OSS_FUZZ_ROOT'], 'infra', 'cifuzz')) +import cifuzz + +# TODO: Turn default logging to INFO when CIFuzz is stable +logging.basicConfig( + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', + level=logging.DEBUG) + + +def main(): + """Runs OSS-Fuzz project's fuzzers for CI tools. + This is the entrypoint for the run_fuzzers github action. + This action can be added to any OSS-Fuzz project's workflow that uses Github. + + NOTE: libfuzzer binaries must be located in the ${GITHUB_WORKSPACE}/out + directory in order for this action to be used. This action will only fuzz the + binary's that are located in that directory. It is reccomended that you add + the build_fuzzers action preceding this one. + + NOTE: Any crash report will be in the filepath: + ${GITHUB_WORKSPACE}/out/testcase + This can be used in parallel with the upload-artifact action to surface the + logs. + + Required environment variables: + FUZZ_SECONDS: The length of time in seconds that fuzzers are to be run. + GITHUB_WORKSPACE: The shared volume directory where input artifacts are. + DRY_RUN: If true, no failures will surface. + + Returns: + 0 on success or 1 on Failure. + """ + fuzz_seconds = int(os.environ.get('FUZZ_SECONDS', 600)) + workspace = os.environ.get('GITHUB_WORKSPACE') + + # Check if failures should not be reported. + dry_run = (os.environ.get('DRY_RUN').lower() == 'true') + + # The default return code when an error occurs. + error_code = 1 + if dry_run: + # A testcase file is required in order for CIFuzz to surface bugs. + # If the file does not exist, the action will crash attempting to upload it. + # The dry run needs this file because it is set to upload a test case both + # on successful runs and on failures. + out_dir = os.path.join(workspace, 'out') + os.makedirs(out_dir, exist_ok=True) + file_handle = open(os.path.join(out_dir, 'testcase'), 'w') + file_handle.write('No bugs detected.') + file_handle.close() + + # Sets the default return code on error to success. + error_code = 0 + + if not workspace: + logging.error('This script needs to be run in the Github action context.') + return error_code + # Run the specified project's fuzzers from the build. + run_status, bug_found = cifuzz.run_fuzzers(fuzz_seconds, workspace) + if not run_status: + logging.error('Error occured while running in workspace %s.', workspace) + return error_code + if bug_found: + logging.info('Bug found.') + if not dry_run: + # Return 2 when a bug was found by a fuzzer causing the CI to fail. + return 2 + return 0 + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/infra/cifuzz/actions/Dockerfile b/infra/cifuzz/cifuzz-base/Dockerfile similarity index 83% rename from infra/cifuzz/actions/Dockerfile rename to infra/cifuzz/cifuzz-base/Dockerfile index 947c3a222..ecb889a01 100644 --- a/infra/cifuzz/actions/Dockerfile +++ b/infra/cifuzz/cifuzz-base/Dockerfile @@ -13,7 +13,6 @@ # limitations under the License. # ################################################################################ -# Docker image to run CIFuzz in. FROM ubuntu:16.04 @@ -36,9 +35,3 @@ RUN apt-get update && apt-get install docker-ce docker-ce-cli containerd.io -y ENV OSS_FUZZ_ROOT=/opt/oss-fuzz RUN git clone https://github.com/google/oss-fuzz.git ${OSS_FUZZ_ROOT} - -# Copies your code file from action repository to the container -COPY entrypoint.py /opt/entrypoint.py - -# Command to execute when the docker container starts up -ENTRYPOINT ["python3", "/opt/entrypoint.py"] diff --git a/infra/cifuzz/cifuzz.py b/infra/cifuzz/cifuzz.py index ce7586e5e..43aab3111 100644 --- a/infra/cifuzz/cifuzz.py +++ b/infra/cifuzz/cifuzz.py @@ -26,6 +26,7 @@ import sys import fuzz_target # pylint: disable=wrong-import-position +# pylint: disable=import-error sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) import build_specified_commit import helper @@ -124,11 +125,10 @@ def build_fuzzers(project_name, return True -def run_fuzzers(project_name, fuzz_seconds, workspace): +def run_fuzzers(fuzz_seconds, workspace): """Runs all fuzzers for a specific OSS-Fuzz project. Args: - project_name: The name of the OSS-Fuzz project being built. fuzz_seconds: The total time allotted for fuzzing. workspace: The location in a shared volume to store a git repo and build artifacts. @@ -156,8 +156,8 @@ def run_fuzzers(project_name, fuzz_seconds, workspace): # Run fuzzers for alotted time. for fuzzer_path in fuzzer_paths: - target = fuzz_target.FuzzTarget(project_name, fuzzer_path, - fuzz_seconds_per_target, out_dir) + target = fuzz_target.FuzzTarget(fuzzer_path, fuzz_seconds_per_target, + out_dir) test_case, stack_trace = target.fuzz() if not test_case or not stack_trace: logging.info('Fuzzer %s, finished running.', target.target_name) diff --git a/infra/cifuzz/cifuzz_test.py b/infra/cifuzz/cifuzz_test.py index 800df1b53..05011ad9c 100644 --- a/infra/cifuzz/cifuzz_test.py +++ b/infra/cifuzz/cifuzz_test.py @@ -124,7 +124,7 @@ class RunFuzzersIntegrationTest(unittest.TestCase): tmp_dir, commit_sha='0b95fe1039ed7c38fea1f97078316bfc1030c523')) self.assertTrue(os.path.exists(os.path.join(out_path, 'do_stuff_fuzzer'))) - run_success, bug_found = cifuzz.run_fuzzers(EXAMPLE_PROJECT, 5, tmp_dir) + run_success, bug_found = cifuzz.run_fuzzers(5, tmp_dir) self.assertTrue(run_success) self.assertTrue(bug_found) @@ -133,7 +133,7 @@ class RunFuzzersIntegrationTest(unittest.TestCase): with tempfile.TemporaryDirectory() as tmp_dir: out_path = os.path.join(tmp_dir, 'out') os.mkdir(out_path) - run_success, bug_found = cifuzz.run_fuzzers(EXAMPLE_PROJECT, 5, tmp_dir) + run_success, bug_found = cifuzz.run_fuzzers(5, tmp_dir) self.assertFalse(run_success) self.assertFalse(bug_found) @@ -142,14 +142,13 @@ class RunFuzzersIntegrationTest(unittest.TestCase): with tempfile.TemporaryDirectory() as tmp_dir: out_path = os.path.join(tmp_dir, 'out') os.mkdir(out_path) - run_success, bug_found = cifuzz.run_fuzzers(EXAMPLE_PROJECT, 0, tmp_dir) + run_success, bug_found = cifuzz.run_fuzzers(0, tmp_dir) self.assertFalse(run_success) self.assertFalse(bug_found) def test_invalid_out_dir(self): """Tests run_fuzzers with an invalid out directory.""" - run_success, bug_found = cifuzz.run_fuzzers(EXAMPLE_PROJECT, 5, - 'not/a/valid/path') + run_success, bug_found = cifuzz.run_fuzzers(5, 'not/a/valid/path') self.assertFalse(run_success) self.assertFalse(bug_found) diff --git a/infra/cifuzz/example_main.yml b/infra/cifuzz/example_main.yml new file mode 100644 index 000000000..576c84a28 --- /dev/null +++ b/infra/cifuzz/example_main.yml @@ -0,0 +1,26 @@ +name: CIFuzz + +on: [push] + +jobs: + Fuzzing: + + runs-on: ubuntu-latest + + steps: + - name: Build Fuzzers + uses: google/oss-fuzz/infra/cifuzz/actions/build_fuzzers@master + with: + project-name: 'example' + dry-run: false + - name: Run Fuzzers + uses: google/oss-fuzz/infra/cifuzz/actions/run_fuzzers@master + with: + fuzz-time: 600 + dry-run: false + - name: Upload Crash + uses: actions/upload-artifact@v1 + if: failure() + with: + name: fuzzer_testcase + path: ./out/testcase diff --git a/infra/cifuzz/fuzz_target.py b/infra/cifuzz/fuzz_target.py index 94eace86f..cfb01a4ed 100644 --- a/infra/cifuzz/fuzz_target.py +++ b/infra/cifuzz/fuzz_target.py @@ -35,24 +35,21 @@ class FuzzTarget: """A class to manage a single fuzz target. Attributes: - project_name: The name of the OSS-Fuzz project the target is associated. target_name: The name of the fuzz target. duration: The length of time in seconds that the target should run. target_path: The location of the fuzz target binary. """ - def __init__(self, project_name, target_path, duration, out_dir): + def __init__(self, target_path, duration, out_dir): """Represents a single fuzz target. Args: - project_name: The OSS-Fuzz project of this target. target_path: The location of the fuzz target binary. duration: The length of time in seconds the target should run. out_dir: The location of where the output from crashes should be stored. """ self.target_name = os.path.basename(target_path) self.duration = duration - self.project_name = project_name self.target_path = target_path self.out_dir = out_dir