Fuzz OSS-Fuzz with Atheris and ClusterFuzzLite (#8985)

This commit is contained in:
jonathanmetzman 2022-11-30 15:37:36 -05:00 committed by GitHub
parent e5f3db69f0
commit a9f9cda4cc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 185 additions and 2 deletions

View File

@ -0,0 +1,23 @@
# Copyright 2022 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-python:v1
COPY . $SRC/oss-fuzz
WORKDIR oss-fuzz
COPY .clusterfuzzlite/build.sh $SRC/
RUN cp .clusterfuzzlite/coverage_atheris_fuzzer.py infra
WORKDIR infra/
RUN pip3 install --upgrade pip
RUN pip3 install -r cifuzz/requirements.txt
RUN cp -r cifuzz/* .

39
.clusterfuzzlite/build.sh Executable file
View File

@ -0,0 +1,39 @@
#!/bin/bash -eu
# Copyright 2022 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.
fuzzer=coverage_atheris_fuzzer.py
fuzzer_basename=$(basename -s .py $fuzzer)
fuzzer_package=${fuzzer_basename}.pkg
# To avoid issues with Python version conflicts, or changes in environment
# over time on the OSS-Fuzz bots, we use pyinstaller to create a standalone
# package. Though not necessarily required for reproducing issues, this is
# required to keep fuzzers working properly in OSS-Fuzz.
pyinstaller --distpath $OUT --onefile --name $fuzzer_package $fuzzer
# Create execution wrapper. Atheris requires that certain libraries are
# preloaded, so this is also done here to ensure compatibility and simplify
# test case reproduction. Since this helper script is what OSS-Fuzz will
# actually execute, it is also always required.
# NOTE: If you are fuzzing python-only code and do not have native C/C++
# extensions, then remove the LD_PRELOAD line below as preloading sanitizer
# library is not required and can lead to unexpected startup crashes.
echo "#!/bin/sh
# LLVMFuzzerTestOneInput for fuzzer detection.
this_dir=\$(dirname \"\$0\")
LD_PRELOAD=\$this_dir/sanitizer_with_fuzzer.so \
ASAN_OPTIONS=\$ASAN_OPTIONS:symbolize=1:external_symbolizer_path=\$this_dir/llvm-symbolizer:detect_leaks=0 \
\$this_dir/$fuzzer_package \$@" > $OUT/$fuzzer_basename
chmod +x $OUT/$fuzzer_basename

View File

@ -0,0 +1,53 @@
#!/usr/bin/python3
# Copyright 2022 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.
import atheris
import json
import os
from unittest import mock
import sys
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
sys.path.append(
os.path.join(os.path.dirname(os.path.abspath(__file__)), 'cifuzz'))
with atheris.instrument_imports():
import get_coverage
REPO_PATH = '/src/curl'
PROJECT_NAME = 'curl'
oss_fuzz_coverage = get_coverage.OSSFuzzCoverage(REPO_PATH, PROJECT_NAME)
def TestOneInput(data):
try:
decoded_json = json.loads(data)
except (json.decoder.JSONDecodeError, UnicodeDecodeError):
# Wart
return oss_fuzz_coverage.get_files_covered_by_target('fuzz-target')
with mock.patch('get_coverage.OSSFuzzCoverage.get_target_coverage',
return_value=decoded_json):
oss_fuzz_coverage.get_files_covered_by_target('fuzz-target')
return 0
def main():
atheris.Setup(sys.argv, TestOneInput)
atheris.Fuzz()
if __name__ == '__main__':
main()

View File

@ -0,0 +1 @@
language: python

49
.github/workflows/cflite_pr.yml vendored Normal file
View File

@ -0,0 +1,49 @@
name: ClusterFuzzLite PR fuzzing
on:
pull_request:
paths:
- 'infra/**'
permissions: read-all
jobs:
PR:
runs-on: ubuntu-latest
concurrency:
group: ${{ github.workflow }}-${{ matrix.sanitizer }}-${{ github.ref }}
cancel-in-progress: true
strategy:
fail-fast: false
matrix:
sanitizer:
- address
# Override this with the sanitizers you want.
# - undefined
# - memory
steps:
- name: Build Fuzzers (${{ matrix.sanitizer }})
id: build
uses: google/clusterfuzzlite/actions/build_fuzzers@v1
with:
language: python
github-token: ${{ secrets.GITHUB_TOKEN }}
sanitizer: ${{ matrix.sanitizer }}
# Optional but recommended: used to only run fuzzers that are affected
# by the PR.
# See later section on "Git repo for storage".
# storage-repo: https://${{ secrets.PERSONAL_ACCESS_TOKEN }}@github.com/OWNER/STORAGE-REPO-NAME.git
# storage-repo-branch: main # Optional. Defaults to "main"
# storage-repo-branch-coverage: gh-pages # Optional. Defaults to "gh-pages".
- name: Run Fuzzers (${{ matrix.sanitizer }})
id: run
uses: google/clusterfuzzlite/actions/run_fuzzers@v1
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
fuzz-seconds: 600
mode: 'code-change'
sanitizer: ${{ matrix.sanitizer }}
# Optional but recommended: used to download the corpus produced by
# batch fuzzing.
# See later section on "Git repo for storage".
# storage-repo: https://${{ secrets.PERSONAL_ACCESS_TOKEN }}@github.com/OWNER/STORAGE-REPO-NAME.git
# storage-repo-branch: main # Optional. Defaults to "main"
# storage-repo-branch-coverage: gh-pages # Optional. Defaults to "gh-pages".

View File

@ -49,7 +49,7 @@ class BaseCoverage:
""" """
target_cov = self.get_target_coverage(target) target_cov = self.get_target_coverage(target)
if not target_cov: if not target_cov:
logging.info('No coverage available for %s', target) logging.info('No coverage available for %s.', target)
return None return None
coverage_per_file = get_coverage_per_file(target_cov) coverage_per_file = get_coverage_per_file(target_cov)
@ -192,7 +192,11 @@ def is_file_covered(file_cov):
def get_coverage_per_file(target_cov): def get_coverage_per_file(target_cov):
"""Returns the coverage per file within |target_cov|.""" """Returns the coverage per file within |target_cov|."""
try:
return target_cov['data'][0]['files'] return target_cov['data'][0]['files']
except (IndexError, TypeError, KeyError):
logging.error('target_cov: %s is malformed.', target_cov)
return None
def _normalize_repo_path(repo_path): def _normalize_repo_path(repo_path):

View File

@ -17,6 +17,7 @@ import json
import unittest import unittest
from unittest import mock from unittest import mock
import parameterized
from pyfakefs import fake_filesystem_unittest from pyfakefs import fake_filesystem_unittest
import pytest import pytest
@ -127,6 +128,19 @@ class OSSFuzzCoverageGetFilesCoveredByTargetTest(unittest.TestCase):
self.oss_fuzz_coverage = get_coverage.OSSFuzzCoverage( self.oss_fuzz_coverage = get_coverage.OSSFuzzCoverage(
REPO_PATH, PROJECT_NAME) REPO_PATH, PROJECT_NAME)
@parameterized.parameterized.expand([({
'data': []
},), ({
'data': [[]]
},), ({
'data': [{}]
},)])
def test_malformed_cov_data(self, coverage_data):
"""Tests that covered files can be retrieved from a coverage report."""
with mock.patch('get_coverage.OSSFuzzCoverage.get_target_coverage',
return_value=coverage_data):
self.oss_fuzz_coverage.get_files_covered_by_target(FUZZ_TARGET)
def test_valid_target(self): def test_valid_target(self):
"""Tests that covered files can be retrieved from a coverage report.""" """Tests that covered files can be retrieved from a coverage report."""
fuzzer_cov_data = _get_example_curl_coverage() fuzzer_cov_data = _get_example_curl_coverage()