Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions src/clusterfuzz/_internal/bot/fuzzers/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"""Fuzzer utils."""

import functools
import json
import os
import re
import stat
Expand Down Expand Up @@ -98,8 +99,53 @@ def is_fuzz_target(file_path, file_opener: Optional[Callable] = None):
return False


def _get_manifest_fuzz_targets(path):
"""Get list of fuzz targets from clusterfuzz_manifest.json if present."""
manifest_path = os.path.join(path, 'clusterfuzz_manifest.json')
if not os.path.exists(manifest_path):
return None

try:
with open(manifest_path, 'r', encoding='utf-8') as f:
manifest = json.load(f)
except Exception as e:
logs.error(f'Failed to read {manifest_path}: {e}')
return None

manifest_targets = manifest.get('fuzz_targets')
if manifest_targets is None:
return None

if not isinstance(manifest_targets, list):
logs.error(f'fuzz_targets in {manifest_path} is not a list')
return None

fuzz_target_paths = []
for target_path in manifest_targets:
if not isinstance(target_path, str):
logs.error(f'Entry in fuzz_targets ({manifest_path}) is not a string: '
f'{target_path}')
continue
file_path = os.path.join(path, target_path)
if not os.path.exists(file_path):
basename_path = os.path.join(path, os.path.basename(target_path))
if os.path.exists(basename_path):
file_path = basename_path

if os.path.exists(file_path) and not os.path.isdir(file_path):
fuzz_target_paths.append(file_path)
else:
logs.warning(f'Fuzz target {target_path} in {manifest_path} not found.')

return fuzz_target_paths if fuzz_target_paths else None


def get_fuzz_targets_local(path):
"""Get list of fuzz targets paths (local)."""
manifest_fuzz_targets = _get_manifest_fuzz_targets(path)
if manifest_fuzz_targets is not None:
return manifest_fuzz_targets

fuzz_target_paths = []

for root, _, files in shell.walk(path):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -314,7 +314,7 @@ def __init__(self,
self._manifest_fuzz_targets = None
# The manifest may not exist for earlier versions of archives. In this
# case, default to schema version 0.
manifest_path = CHROME_MANIFEST_FILENAME
manifest_path = os.path.join(reader.root_dir(), CHROME_MANIFEST_FILENAME)
if not self.file_exists(manifest_path):
self._archive_schema_version = default_archive_schema_version
return
Expand Down
42 changes: 42 additions & 0 deletions src/clusterfuzz/_internal/tests/core/bot/fuzzers/utils_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
# limitations under the License.
"""Tests for fuzzers.utils."""

import json
import os
import shutil
import tempfile
Expand Down Expand Up @@ -148,3 +149,44 @@ def test_par(self):
"""Test par extension."""
self.assertEqual('/a/b.labels',
utils.get_supporting_file('/a/b.par', '.labels'))


class GetFuzzTargetsLocalTest(unittest.TestCase):
"""Tests for get_fuzz_targets_local."""

def setUp(self):
test_helpers.patch_environ(self)
self.temp_dir = tempfile.mkdtemp()

def tearDown(self):
shutil.rmtree(self.temp_dir, ignore_errors=True)

def _create_file(self, name, contents=b''):
path = os.path.join(self.temp_dir, name)
os.makedirs(os.path.dirname(path), exist_ok=True)
with open(path, 'wb') as f:
f.write(contents)
return path

def test_manifest_targets_used(self):
"""Test that clusterfuzz_manifest.json is used to find targets."""
target_a = self._create_file('target_a', contents=b'LLVMFuzzerTestOneInput')
self._create_file('run_target_a_fuzzer', contents=b'LLVMFuzzerTestOneInput')
manifest_contents = json.dumps({
'archive_schema_version': 1,
'fuzz_targets': ['target_a']
}).encode('utf-8')
self._create_file('clusterfuzz_manifest.json', contents=manifest_contents)

targets = utils.get_fuzz_targets_local(self.temp_dir)
self.assertEqual([target_a], targets)

def test_manifest_missing_fallback(self):
"""Test fallback to scanning when manifest is missing."""
target_a = self._create_file(
'target_a_fuzzer', contents=b'LLVMFuzzerTestOneInput')
target_b = self._create_file(
'target_b_fuzzer', contents=b'LLVMFuzzerTestOneInput')

targets = utils.get_fuzz_targets_local(self.temp_dir)
self.assertCountEqual([target_a, target_b], targets)
Original file line number Diff line number Diff line change
Expand Up @@ -445,6 +445,25 @@ def test_manifest_fuzz_targets_list(self):
# Ensure list_members was never called for fuzz target discovery.
self.mock_archive_reader.list_members.assert_not_called()

def test_manifest_fuzz_targets_with_root_dir(self):
"""Tests that manifest is read when archive has a root directory prefix."""
self.mock_archive_reader.root_dir.return_value = 'build'

def _mock_file_exists(_, path):
return path == 'build/clusterfuzz_manifest.json'

self.mock.file_exists.side_effect = _mock_file_exists
self._generate_manifest({
'archive_schema_version': 1,
'fuzz_targets': ['out/build/my_fuzzer', 'out/build/other_fuzzer']
})

test_archive = build_archive.ChromeBuildArchive(self.mock_archive_reader)

self.assertEqual(test_archive.archive_schema_version(), 1)
self.assertCountEqual(test_archive.list_fuzz_targets(),
['my_fuzzer', 'other_fuzzer'])

def test_manifest_fuzz_targets_invalid(self):
"""Tests that invalid fuzz_targets (e.g. dict) in the manifest are ignored
and we fallback to discovery."""
Expand Down
Loading