Skip to content

Commit 828343c

Browse files
committed
Support fuzz_targets property in clusterfuzz_manifest.json
1 parent a226d62 commit 828343c

5 files changed

Lines changed: 116 additions & 10 deletions

File tree

src/clusterfuzz/_internal/build_management/build_archive.py

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -141,17 +141,17 @@ class DefaultBuildArchive(BuildArchive):
141141

142142
def __init__(self, reader: archive.ArchiveReader):
143143
super().__init__(reader)
144-
self._fuzz_targets = {}
144+
self._fuzz_targets = None
145145

146146
def get_path_for_target(self, fuzz_target: str) -> str:
147147
"""Returns the path in the archive of the fuzz_target if found.
148148
This is needed because target name normalization means we're losing
149149
information about the actual file reprensenting the fuzz_target.
150150
"""
151-
if not self._fuzz_targets:
151+
if self._fuzz_targets is None:
152152
self.list_fuzz_targets()
153153

154-
if not fuzz_target in self._fuzz_targets:
154+
if not self._fuzz_targets or fuzz_target not in self._fuzz_targets:
155155
return None
156156

157157
return self._fuzz_targets[fuzz_target]
@@ -185,8 +185,10 @@ def get_target_dependencies(
185185
return to_extract
186186

187187
def list_fuzz_targets(self) -> List[str]:
188-
if self._fuzz_targets:
188+
if self._fuzz_targets is not None:
189189
return list(self._fuzz_targets.keys())
190+
191+
self._fuzz_targets = {}
190192
# Import here as this path is not available in App Engine context.
191193
from clusterfuzz._internal.bot.fuzzers import utils as fuzzer_utils
192194

@@ -304,6 +306,17 @@ def __init__(self,
304306
'clusterfuzz_manifest.json was incorrectly formatted or missing an '
305307
'archive_schema_version field')
306308
self._archive_schema_version = default_archive_schema_version
309+
310+
fuzz_targets_data = manifest.get('fuzz_targets')
311+
if isinstance(fuzz_targets_data, list):
312+
self._fuzz_targets = {}
313+
from clusterfuzz._internal.bot.fuzzers import utils as fuzzer_utils
314+
for target_path in fuzz_targets_data:
315+
if isinstance(target_path, str):
316+
fuzz_target = fuzzer_utils.normalize_target_name(target_path)
317+
self._fuzz_targets[fuzz_target] = target_path
318+
elif fuzz_targets_data is not None:
319+
logs.error('fuzz_targets in clusterfuzz_manifest.json is not a list')
307320
else:
308321
self._archive_schema_version = default_archive_schema_version
309322

src/clusterfuzz/_internal/build_management/build_manager.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -522,6 +522,19 @@ def _unpack_build(self,
522522
with self._open_build_archive(base_build_dir, build_dir, build_url,
523523
http_build_url) as build:
524524
unpack_start_time = time.time()
525+
if environment.is_engine_fuzzer_job() and not self.fuzz_target:
526+
# If no fuzz target is selected (e.g. during initial fuzz target
527+
# discovery), we only need the list of fuzz targets. We get them
528+
# from the build archive (which instantly reads
529+
# clusterfuzz_manifest.json if present) and skip unpacking the
530+
# archive to disk entirely to prevent out of disk space errors.
531+
list_fuzz_target_start_time = time.time()
532+
self._fuzz_targets = list(build.list_fuzz_targets())
533+
_emit_job_build_retrieval_metric(list_fuzz_target_start_time,
534+
'list_fuzz_targets',
535+
self._build_type)
536+
logs.info('No fuzz target selected; skipping unpack for discovery.')
537+
return True
525538
if not self._unpack_everything:
526539
# We will never unpack the full build so we need to get the targets
527540
# from the build archive.
@@ -1351,7 +1364,8 @@ def setup_regular_build(revision,
13511364
build.build_dir, # Store inside the main build.
13521365
revision,
13531366
extra_build_url,
1354-
build_prefix=fuzzer_utils.EXTRA_BUILD_DIR)
1367+
build_prefix=fuzzer_utils.EXTRA_BUILD_DIR,
1368+
fuzz_target=fuzz_target)
13551369
if not build.setup():
13561370
return None
13571371

src/clusterfuzz/_internal/tests/core/bot/testcase_manager_test.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -904,7 +904,7 @@ def test_reproduce(self):
904904

905905
self._setup_env(job_type='libfuzzer_asan_job')
906906

907-
build_manager.setup_build()
907+
build_manager.setup_build(fuzz_target='test_fuzzer')
908908
result = testcase_manager.engine_reproduce(
909909
libfuzzer_engine.Engine(), 'test_fuzzer', testcase_file_path, [], 30)
910910

src/clusterfuzz/_internal/tests/core/build_management/build_archive_test.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -409,3 +409,58 @@ def test_manifest_is_correctly_read(self):
409409
self._generate_manifest(1)
410410
test_archive = build_archive.ChromeBuildArchive(self.mock.open.return_value)
411411
self.assertEqual(test_archive.archive_schema_version(), 1)
412+
413+
def _generate_manifest_with_fuzz_targets(self, archive_schema_version,
414+
fuzz_targets):
415+
"""Mocks open calls so that they return a buffer containing valid JSON for
416+
the given archive schema version and fuzz_targets."""
417+
418+
def _mock_open(_):
419+
buffer = io.BytesIO(b'')
420+
buffer.write(
421+
json.dumps({
422+
'archive_schema_version': archive_schema_version,
423+
'fuzz_targets': fuzz_targets,
424+
}).encode())
425+
buffer.seek(0)
426+
return buffer
427+
428+
self.mock.open.return_value.open.side_effect = _mock_open
429+
430+
def test_manifest_fuzz_targets_list(self):
431+
"""Tests that fuzz_targets specified as a list in the manifest correctly
432+
populates self._fuzz_targets and bypasses discovery."""
433+
self.mock.file_exists.return_value = True
434+
self._generate_manifest_with_fuzz_targets(
435+
1, ['out/build/my_fuzzer', 'out/build/other_fuzzer'])
436+
test_archive = build_archive.ChromeBuildArchive(self.mock.open.return_value)
437+
self.assertEqual(test_archive.archive_schema_version(), 1)
438+
self.assertCountEqual(test_archive.list_fuzz_targets(),
439+
['my_fuzzer', 'other_fuzzer'])
440+
self.assertEqual(
441+
test_archive.get_path_for_target('my_fuzzer'), 'out/build/my_fuzzer')
442+
# Ensure list_members was never called for fuzz target discovery.
443+
self.mock.open.return_value.list_members.assert_not_called()
444+
445+
def test_manifest_fuzz_targets_invalid(self):
446+
"""Tests that invalid fuzz_targets (e.g. dict) in the manifest are ignored
447+
and we fallback to discovery."""
448+
self.mock.file_exists.return_value = True
449+
self._generate_manifest_with_fuzz_targets(
450+
1, {'my_fuzzer': 'out/build/my_fuzzer'})
451+
self.mock.open.return_value.list_members.return_value = []
452+
test_archive = build_archive.ChromeBuildArchive(self.mock.open.return_value)
453+
self.assertEqual(test_archive.archive_schema_version(), 1)
454+
test_archive.list_fuzz_targets()
455+
self.mock.open.return_value.list_members.assert_called_once()
456+
457+
def test_manifest_fuzz_targets_missing(self):
458+
"""Tests that missing fuzz_targets in the manifest are ignored
459+
and we fallback to discovery."""
460+
self.mock.file_exists.return_value = True
461+
self._generate_manifest(1)
462+
self.mock.open.return_value.list_members.return_value = []
463+
test_archive = build_archive.ChromeBuildArchive(self.mock.open.return_value)
464+
self.assertEqual(test_archive.archive_schema_version(), 1)
465+
test_archive.list_fuzz_targets()
466+
self.mock.open.return_value.list_members.assert_called_once()

src/clusterfuzz/_internal/tests/core/build_management/build_manager_test.py

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -532,6 +532,26 @@ def _assert_env_vars(self):
532532
os.environ['BUILD_DIR'],
533533
'/builds/path_be4c9ca0267afcd38b7c1a3eebb5998d0908f025/revisions')
534534

535+
def test_setup_fuzz_discovery(self):
536+
"""Tests setting up a build during initial fuzz target discovery.
537+
538+
(fuzz_target=None).
539+
"""
540+
os.environ['TASK_NAME'] = 'fuzz'
541+
self.mock.time.return_value = 1000.0
542+
build = build_manager.setup_regular_build(2, fuzz_target=None)
543+
self.assertIsInstance(build, build_manager.RegularBuild)
544+
self.assertEqual(_get_timestamp(build.base_build_dir), 1000.0)
545+
546+
self._assert_env_vars()
547+
self.assertEqual(os.environ['APP_REVISION'], '2')
548+
549+
# Verify unpack() was never called because we skipped unpacking
550+
# for discovery.
551+
unpack_mock = self.mock.open.return_value.__enter__.return_value.unpack
552+
self.assertEqual(unpack_mock.call_count, 0)
553+
self.assertCountEqual(['target1', 'target2', 'target3'], build.fuzz_targets)
554+
535555
@parameterized.parameterized.expand(['True', 'False'])
536556
def test_setup_fuzz(self, unpack_all):
537557
"""Tests setting up a build during fuzzing."""
@@ -741,7 +761,9 @@ def test_setup_fuzz_over_http(self):
741761
os.environ['ALLOW_UNPACK_OVER_HTTP'] = "True"
742762
self.mock.unzip_over_http_compatible.return_value = True
743763
self.mock.time.return_value = 1000.0
744-
build = build_manager.setup_regular_build(2)
764+
fuzz_target = build_manager.pick_random_fuzz_target(
765+
target_weights=self.target_weights)
766+
build = build_manager.setup_regular_build(2, fuzz_target=fuzz_target)
745767
self.assertIsInstance(build, build_manager.RegularBuild)
746768
self.assertEqual(_get_timestamp(build.base_build_dir), 1000.0)
747769

@@ -756,7 +778,7 @@ def test_setup_fuzz_over_http(self):
756778

757779
# Test setting up build again.
758780
self.mock.time.return_value = 1005.0
759-
build = build_manager.setup_regular_build(2)
781+
build = build_manager.setup_regular_build(2, fuzz_target=fuzz_target)
760782

761783
self.assertIsInstance(build, build_manager.RegularBuild)
762784

@@ -778,7 +800,9 @@ def test_setup_fuzz_over_http_unpack_all(self):
778800
'https://example.com/path/file-release-([0-9]+).zip')
779801
self.mock.unzip_over_http_compatible.return_value = True
780802
self.mock.time.return_value = 1000.0
781-
build = build_manager.setup_regular_build(2)
803+
fuzz_target = build_manager.pick_random_fuzz_target(
804+
target_weights=self.target_weights)
805+
build = build_manager.setup_regular_build(2, fuzz_target=fuzz_target)
782806
self.assertIsInstance(build, build_manager.RegularBuild)
783807
self.assertEqual(_get_timestamp(build.base_build_dir), 1000.0)
784808

@@ -791,7 +815,7 @@ def test_setup_fuzz_over_http_unpack_all(self):
791815
# Test setting up build again.
792816
os.environ['FUZZ_TARGET'] = ''
793817
self.mock.time.return_value = 1005.0
794-
build = build_manager.setup_regular_build(2)
818+
build = build_manager.setup_regular_build(2, fuzz_target=fuzz_target)
795819

796820
self.assertIsInstance(build, build_manager.RegularBuild)
797821

0 commit comments

Comments
 (0)