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
2 changes: 2 additions & 0 deletions src/clusterfuzz/_internal/build_management/build_archive.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
'afl-showmap',
'afl-tmin',
'centipede',
'clusterfuzz_manifest.json',
'honggfuzz',
'jazzer_agent_deploy.jar',
'jazzer_driver',
Expand Down Expand Up @@ -421,6 +422,7 @@ def _get_common_files(self) -> List[str]:
return [
'args.gn',
'llvm-symbolizer',
'clusterfuzz_manifest.json',
]

def get_target_dependencies(
Expand Down
60 changes: 46 additions & 14 deletions src/clusterfuzz/_internal/build_management/build_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from collections import namedtuple
import contextlib
import datetime
import json
import os
import re
import shutil
Expand Down Expand Up @@ -378,6 +379,19 @@ def _pre_setup(self):
environment.set_value(self.env_prefix + 'APP_PATH', '')
environment.set_value(self.env_prefix + 'APP_PATH_DEBUG', '')

def _read_schema_version_from_manifest(self, build_dir: str) -> int:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AFAICT this method does not use self. Move it to a helper outside the class, or annotate it as @staticmethod.

"""Reads archive_schema_version from clusterfuzz_manifest.json."""
manifest_path = os.path.join(build_dir, 'clusterfuzz_manifest.json')
if not os.path.exists(manifest_path):
return 0
try:
with open(manifest_path) as f:
manifest = json.load(f)
return int(manifest.get('archive_schema_version') or 0)
except Exception as e:
logs.warning(f'Failed to read schema version from {manifest_path}: {e}')
return 0

def _patch_rpath(self, binary_path, instrumented_library_paths):
"""Patch rpaths of a binary to point to instrumented libraries"""
rpaths = get_rpaths(binary_path)
Expand All @@ -390,22 +404,39 @@ def _patch_rpath(self, binary_path, instrumented_library_paths):

set_rpaths(binary_path, rpaths)

def _patch_rpaths(self, instrumented_library_paths):
def _patch_rpaths(self,
build_dir: str | None = None,
app_path_env: str | None = None):
Comment on lines +407 to +409

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be a bit easier to follow if build_dir was always supplied (and we did not have a fallback for app_path_env = None). Let's move this method (and _patch_rpath()) outside this class or annotate them as @staticmethod, then call it with self.build_dir, self.release_build_dir, or self.debug_build_dir:

def _patch_rpaths(build_dir: str, app_path_env: str):
  ...
  if environment.is_engine_fuzzer_job():
    ...

  assert app_path_env
  ...

"""Patch rpaths of builds to point to instrumented libraries."""
instrumented_library_paths = environment.get_instrumented_libraries_paths()
if not instrumented_library_paths:
return

if not build_dir:
build_dir = self.build_dir

schema_version = self._read_schema_version_from_manifest(build_dir)
if schema_version > 0:
logs.info('Skipping RPATH patching for schema v1+ build.')
return

if environment.is_engine_fuzzer_job():
# Import here as this path is not available in App Engine context.
from clusterfuzz._internal.bot.fuzzers import utils as fuzzer_utils

for target_path in fuzzer_utils.get_fuzz_targets(self.build_dir):
for target_path in fuzzer_utils.get_fuzz_targets(build_dir):
self._patch_rpath(target_path, instrumented_library_paths)
else:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider an early return here to reduce nesting.

app_path = environment.get_value('APP_PATH')
if app_path:
self._patch_rpath(app_path, instrumented_library_paths)

app_path_debug = environment.get_value('APP_PATH_DEBUG')
if app_path_debug:
self._patch_rpath(app_path_debug, instrumented_library_paths)
if app_path_env:
app_paths = [environment.get_value(app_path_env)]
else:
app_paths = [
environment.get_value('APP_PATH'),
environment.get_value('APP_PATH_DEBUG')
]
Comment on lines +433 to +436

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we ever want to fix both at once now that we're calling this method twice for symbolized builds. See also my comment above.

for app_path in app_paths:
if app_path:
self._patch_rpath(app_path, instrumented_library_paths)

def _post_setup_success(self, update_revision=True):
"""Common post-setup."""
Expand All @@ -417,11 +448,6 @@ def _post_setup_success(self, update_revision=True):
timestamp_file_path = os.path.join(self.base_build_dir, TIMESTAMP_FILE)
utils.write_data_to_file(time.time(), timestamp_file_path)

# Update rpaths if necessary (for e.g. instrumented libraries).
instrumented_library_paths = environment.get_instrumented_libraries_paths()
if instrumented_library_paths:
self._patch_rpaths(instrumented_library_paths)

@contextlib.contextmanager
def _download_and_open_build_archive(self, base_build_dir: str,
build_dir: str, build_url: str):
Expand Down Expand Up @@ -769,6 +795,7 @@ def setup(self):

self._setup_application_path(build_update=build_update)
self._post_setup_success(update_revision=build_update)
self._patch_rpaths()
Comment on lines 796 to +798

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

setup_application_path sets up APP_PATH by default 1, so I don't think we should try to patch APP_PATH and APP_PATH_DEBUG in this case. Combined with my comment above, let's do:

_patch_rpaths(self.build_dir, 'APP_PATH')

return True


Expand Down Expand Up @@ -879,12 +906,16 @@ def setup(self):
self._setup_application_path(
self.release_build_dir, build_update=build_update)
environment.set_value('BUILD_URL', self.release_build_url)
self._patch_rpaths(
build_dir=self.release_build_dir, app_path_env='APP_PATH')

if self.debug_build_url:
# Note: this will override LLVM_SYMBOLIZER_PATH, APP_DIR etc from the
# previous release setup, which may not be desirable behaviour.
self._setup_application_path(
self.debug_build_dir, 'APP_PATH_DEBUG', build_update=build_update)
self._patch_rpaths(
build_dir=self.debug_build_dir, app_path_env='APP_PATH_DEBUG')

self._post_setup_success(update_revision=build_update)
return True
Expand Down Expand Up @@ -989,6 +1020,7 @@ def setup(self):
self._fuzz_targets = list(self._get_fuzz_targets_from_dir(self.build_dir))
self._setup_application_path(build_update=build_update)
self._post_setup_success(update_revision=build_update)
self._patch_rpaths()
return True


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1580,6 +1580,32 @@ def test_patch_rpaths_existing_msan(self):
rpaths = build_manager.get_rpaths(os.environ['APP_PATH'])
self.assertListEqual(['/msan/lib', '/msan/usr/lib'], rpaths)

def test_patch_rpaths_schema_v1(self):
"""Tests that RPATH is not patched when schema version is > 0."""

def mock_unpack_build_schema_v1(actual_self,
base_build_dir,
build_dir,
url,
http_build_url=None):
self.mock_unpack_build('rpath_new', actual_self, base_build_dir,
build_dir, url, http_build_url)
manifest_path = os.path.join(build_dir, 'clusterfuzz_manifest.json')
utils.write_data_to_file('{"archive_schema_version": 1}', manifest_path)
return True

self.mock._unpack_build.side_effect = mock_unpack_build_schema_v1

build = build_manager.RegularBuild(self.base_build_dir, 1337,
'chained-origins')
self.assertTrue(build.setup())
self.assertEqual(
os.path.join(self.base_build_dir, 'revisions', 'app'),
os.environ['APP_PATH'])

rpaths = build_manager.get_rpaths(os.environ['APP_PATH'])
self.assertListEqual([], rpaths)


class SortBuildUrlsByRevisionTest(unittest.TestCase):
"""Test _sort_build_urls_by_revision."""
Expand Down Expand Up @@ -1969,3 +1995,46 @@ def test_is_discovery_non_fuzzing(self):
self.mock.is_engine_fuzzer_job.return_value = False
build = build_manager.Build('/base', 1, build_prefix='', fuzz_target=None)
self.assertFalse(build.is_discovery)


class ReadSchemaVersionFromManifestTest(fake_filesystem_unittest.TestCase):
"""Tests _read_schema_version_from_manifest resolution."""

def setUp(self):
fake_filesystem_unittest.TestCase.setUp(self)
self.setUpPyfakefs()
self.base_build_dir = '/base_build_dir'
self.fs.create_dir(self.base_build_dir)

def test_regular_build(self):
"""Tests reading schema version from manifest for regular build."""
build = build_manager.RegularBuild(self.base_build_dir, 1337,
'gs://dummy/url.zip')
self.fs.create_file(
os.path.join(build.build_dir, 'clusterfuzz_manifest.json'),
contents='{"archive_schema_version": 1}')
self.assertEqual(
build._read_schema_version_from_manifest(build.build_dir), 1)

def test_symbolized_build(self):
"""Tests reading schema version from manifest for release and debug dirs."""
build = build_manager.SymbolizedBuild(self.base_build_dir, 1337,
'gs://dummy/release.zip',
'gs://dummy/debug.zip')
self.fs.create_file(
os.path.join(build.release_build_dir, 'clusterfuzz_manifest.json'),
contents='{"archive_schema_version": 1}')
self.fs.create_file(
os.path.join(build.debug_build_dir, 'clusterfuzz_manifest.json'),
contents='{"archive_schema_version": 2}')
self.assertEqual(
build._read_schema_version_from_manifest(build.release_build_dir), 1)
self.assertEqual(
build._read_schema_version_from_manifest(build.debug_build_dir), 2)

def test_missing_file(self):
"""Tests that 0 is returned if manifest file is missing."""
build = build_manager.RegularBuild(self.base_build_dir, 1337,
'gs://dummy/url.zip')
self.assertEqual(
build._read_schema_version_from_manifest(build.build_dir), 0)
Loading