Skip to content

Commit a9f9cda

Browse files
Fuzz OSS-Fuzz with Atheris and ClusterFuzzLite (#8985)
1 parent e5f3db6 commit a9f9cda

File tree

7 files changed

+185
-2
lines changed

7 files changed

+185
-2
lines changed

.clusterfuzzlite/Dockerfile

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Copyright 2022 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
FROM gcr.io/oss-fuzz-base/base-builder-python:v1
16+
COPY . $SRC/oss-fuzz
17+
WORKDIR oss-fuzz
18+
COPY .clusterfuzzlite/build.sh $SRC/
19+
RUN cp .clusterfuzzlite/coverage_atheris_fuzzer.py infra
20+
WORKDIR infra/
21+
RUN pip3 install --upgrade pip
22+
RUN pip3 install -r cifuzz/requirements.txt
23+
RUN cp -r cifuzz/* .

.clusterfuzzlite/build.sh

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
#!/bin/bash -eu
2+
# Copyright 2022 Google LLC
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
16+
fuzzer=coverage_atheris_fuzzer.py
17+
fuzzer_basename=$(basename -s .py $fuzzer)
18+
fuzzer_package=${fuzzer_basename}.pkg
19+
20+
# To avoid issues with Python version conflicts, or changes in environment
21+
# over time on the OSS-Fuzz bots, we use pyinstaller to create a standalone
22+
# package. Though not necessarily required for reproducing issues, this is
23+
# required to keep fuzzers working properly in OSS-Fuzz.
24+
pyinstaller --distpath $OUT --onefile --name $fuzzer_package $fuzzer
25+
26+
# Create execution wrapper. Atheris requires that certain libraries are
27+
# preloaded, so this is also done here to ensure compatibility and simplify
28+
# test case reproduction. Since this helper script is what OSS-Fuzz will
29+
# actually execute, it is also always required.
30+
# NOTE: If you are fuzzing python-only code and do not have native C/C++
31+
# extensions, then remove the LD_PRELOAD line below as preloading sanitizer
32+
# library is not required and can lead to unexpected startup crashes.
33+
echo "#!/bin/sh
34+
# LLVMFuzzerTestOneInput for fuzzer detection.
35+
this_dir=\$(dirname \"\$0\")
36+
LD_PRELOAD=\$this_dir/sanitizer_with_fuzzer.so \
37+
ASAN_OPTIONS=\$ASAN_OPTIONS:symbolize=1:external_symbolizer_path=\$this_dir/llvm-symbolizer:detect_leaks=0 \
38+
\$this_dir/$fuzzer_package \$@" > $OUT/$fuzzer_basename
39+
chmod +x $OUT/$fuzzer_basename
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
#!/usr/bin/python3
2+
# Copyright 2022 Google LLC
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
16+
import atheris
17+
import json
18+
import os
19+
from unittest import mock
20+
import sys
21+
22+
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
23+
sys.path.append(
24+
os.path.join(os.path.dirname(os.path.abspath(__file__)), 'cifuzz'))
25+
26+
with atheris.instrument_imports():
27+
import get_coverage
28+
29+
REPO_PATH = '/src/curl'
30+
PROJECT_NAME = 'curl'
31+
oss_fuzz_coverage = get_coverage.OSSFuzzCoverage(REPO_PATH, PROJECT_NAME)
32+
33+
34+
def TestOneInput(data):
35+
try:
36+
decoded_json = json.loads(data)
37+
except (json.decoder.JSONDecodeError, UnicodeDecodeError):
38+
# Wart
39+
return oss_fuzz_coverage.get_files_covered_by_target('fuzz-target')
40+
41+
with mock.patch('get_coverage.OSSFuzzCoverage.get_target_coverage',
42+
return_value=decoded_json):
43+
oss_fuzz_coverage.get_files_covered_by_target('fuzz-target')
44+
return 0
45+
46+
47+
def main():
48+
atheris.Setup(sys.argv, TestOneInput)
49+
atheris.Fuzz()
50+
51+
52+
if __name__ == '__main__':
53+
main()

.clusterfuzzlite/project.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
language: python

.github/workflows/cflite_pr.yml

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
name: ClusterFuzzLite PR fuzzing
2+
on:
3+
pull_request:
4+
paths:
5+
- 'infra/**'
6+
permissions: read-all
7+
jobs:
8+
PR:
9+
runs-on: ubuntu-latest
10+
concurrency:
11+
group: ${{ github.workflow }}-${{ matrix.sanitizer }}-${{ github.ref }}
12+
cancel-in-progress: true
13+
strategy:
14+
fail-fast: false
15+
matrix:
16+
sanitizer:
17+
- address
18+
# Override this with the sanitizers you want.
19+
# - undefined
20+
# - memory
21+
steps:
22+
- name: Build Fuzzers (${{ matrix.sanitizer }})
23+
id: build
24+
uses: google/clusterfuzzlite/actions/build_fuzzers@v1
25+
with:
26+
language: python
27+
github-token: ${{ secrets.GITHUB_TOKEN }}
28+
sanitizer: ${{ matrix.sanitizer }}
29+
# Optional but recommended: used to only run fuzzers that are affected
30+
# by the PR.
31+
# See later section on "Git repo for storage".
32+
# storage-repo: https://${{ secrets.PERSONAL_ACCESS_TOKEN }}@github.com/OWNER/STORAGE-REPO-NAME.git
33+
# storage-repo-branch: main # Optional. Defaults to "main"
34+
# storage-repo-branch-coverage: gh-pages # Optional. Defaults to "gh-pages".
35+
- name: Run Fuzzers (${{ matrix.sanitizer }})
36+
id: run
37+
uses: google/clusterfuzzlite/actions/run_fuzzers@v1
38+
with:
39+
github-token: ${{ secrets.GITHUB_TOKEN }}
40+
fuzz-seconds: 600
41+
mode: 'code-change'
42+
sanitizer: ${{ matrix.sanitizer }}
43+
# Optional but recommended: used to download the corpus produced by
44+
# batch fuzzing.
45+
# See later section on "Git repo for storage".
46+
# storage-repo: https://${{ secrets.PERSONAL_ACCESS_TOKEN }}@github.com/OWNER/STORAGE-REPO-NAME.git
47+
# storage-repo-branch: main # Optional. Defaults to "main"
48+
# storage-repo-branch-coverage: gh-pages # Optional. Defaults to "gh-pages".
49+

infra/cifuzz/get_coverage.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ def get_files_covered_by_target(self, target):
4949
"""
5050
target_cov = self.get_target_coverage(target)
5151
if not target_cov:
52-
logging.info('No coverage available for %s', target)
52+
logging.info('No coverage available for %s.', target)
5353
return None
5454

5555
coverage_per_file = get_coverage_per_file(target_cov)
@@ -192,7 +192,11 @@ def is_file_covered(file_cov):
192192

193193
def get_coverage_per_file(target_cov):
194194
"""Returns the coverage per file within |target_cov|."""
195-
return target_cov['data'][0]['files']
195+
try:
196+
return target_cov['data'][0]['files']
197+
except (IndexError, TypeError, KeyError):
198+
logging.error('target_cov: %s is malformed.', target_cov)
199+
return None
196200

197201

198202
def _normalize_repo_path(repo_path):

infra/cifuzz/get_coverage_test.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import unittest
1818
from unittest import mock
1919

20+
import parameterized
2021
from pyfakefs import fake_filesystem_unittest
2122
import pytest
2223

@@ -127,6 +128,19 @@ def setUp(self):
127128
self.oss_fuzz_coverage = get_coverage.OSSFuzzCoverage(
128129
REPO_PATH, PROJECT_NAME)
129130

131+
@parameterized.parameterized.expand([({
132+
'data': []
133+
},), ({
134+
'data': [[]]
135+
},), ({
136+
'data': [{}]
137+
},)])
138+
def test_malformed_cov_data(self, coverage_data):
139+
"""Tests that covered files can be retrieved from a coverage report."""
140+
with mock.patch('get_coverage.OSSFuzzCoverage.get_target_coverage',
141+
return_value=coverage_data):
142+
self.oss_fuzz_coverage.get_files_covered_by_target(FUZZ_TARGET)
143+
130144
def test_valid_target(self):
131145
"""Tests that covered files can be retrieved from a coverage report."""
132146
fuzzer_cov_data = _get_example_curl_coverage()

0 commit comments

Comments
 (0)