[CIFuzz] Separate build and run actions ()

* Separating actions into build and run

* Formatting

* Formatting

* Removing project-name from run action

* Jonathan comments

* Maxs comments pt.1

* Updating example_main.yml

* Switch 1 0 to true false

* Add base Dockerfile for CIFuzz

* Adding license to dockerfile
This commit is contained in:
Leo Neat 2020-02-06 13:39:43 -08:00 committed by GitHub
parent 69f75431fe
commit 5cefaac000
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 198 additions and 52 deletions

View File

@ -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"]

View File

@ -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}}

View File

@ -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

View File

@ -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"]

View File

@ -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}}

View File

@ -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())

View File

@ -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"]

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -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