[CIFuzz] Add support for external projects to helper.py (#6141)

Add support to helper.py for external projects for all subcommands except `download_corpora`.
External users just need to specify `build_integration_path`, `project_src_path` and `external`.

Also do a big refactor to pass a project object as part of args instead of a string containing the name of the project or its
path.
Related: #6125
This commit is contained in:
jonathanmetzman 2021-08-04 06:42:17 -07:00 committed by GitHub
parent 4f34600051
commit d88b5e4aa7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 419 additions and 181 deletions

View File

@ -105,7 +105,7 @@ def main():
architecture=args.architecture)
result = bisect(args.type, args.old_commit, args.new_commit,
args.testcase_path, args.fuzz_target, build_data)
args.test_case_path, args.fuzz_target, build_data)
if not result.commit:
logging.error('No error was found in commit range %s:%s', args.old_commit,
args.new_commit)
@ -142,12 +142,15 @@ def _check_for_crash(project_name, fuzz_target, testcase_path):
return utils.execute(command + args)
logging.info('Checking for crash')
out, err, return_code = helper.reproduce_impl(project_name,
fuzz_target,
False, [], [],
testcase_path,
run_function=docker_run,
err_result=(None, None, None))
out, err, return_code = helper.reproduce_impl(
project=helper.Project(project_name),
fuzzer_name=fuzz_target,
valgrind=False,
env_to_add=[],
fuzzer_args=[],
testcase_path=testcase_path,
run_function=docker_run,
err_result=(None, None, None))
if return_code is None:
return None

View File

@ -147,7 +147,7 @@ def copy_src_from_docker(project_name, host_dir):
@retry.wrap(_IMAGE_BUILD_TRIES, 2)
def _build_image_with_retries(project_name):
"""Build image with retries."""
return helper.build_image_impl(project_name)
return helper.build_image_impl(helper.Project(project_name))
def get_required_post_checkout_steps(dockerfile_path):
@ -223,7 +223,8 @@ def build_fuzzers_from_commit(commit,
post_checkout_step,
])
result = helper.build_fuzzers_impl(project_name=build_data.project_name,
project = helper.Project(build_data.project_name)
result = helper.build_fuzzers_impl(project=project,
clean=True,
engine=build_data.engine,
sanitizer=build_data.sanitizer,

View File

@ -61,7 +61,8 @@ class BuildImageIntegrationTest(unittest.TestCase):
build_specified_commit.build_fuzzers_from_commit(test_repo.old_commit,
test_repo_manager,
host_src_dir, build_data)
old_result = helper.reproduce_impl(project_name=test_repo.project_name,
project = helper.Project(test_repo.project_name)
old_result = helper.reproduce_impl(project=project,
fuzzer_name=test_repo.fuzz_target,
valgrind=False,
env_to_add=[],
@ -70,9 +71,12 @@ class BuildImageIntegrationTest(unittest.TestCase):
build_specified_commit.build_fuzzers_from_commit(test_repo.project_name,
test_repo_manager,
host_src_dir, build_data)
new_result = helper.reproduce_impl(test_repo.project_name,
test_repo.fuzz_target, False, [], [],
test_repo.testcase_path)
new_result = helper.reproduce_impl(project=project,
fuzzer_name=test_repo.fuzz_target,
valgrind=False,
env_to_add=[],
fuzzer_args=[],
testcase_path=test_repo.testcase_path)
self.assertNotEqual(new_result, old_result)
def test_detect_main_repo_from_commit(self):

View File

@ -63,9 +63,70 @@ LANGUAGES_WITH_COVERAGE_SUPPORT = ['c', 'c++', 'go', 'jvm', 'rust']
WORKDIR_REGEX = re.compile(r'\s*WORKDIR\s*([^\s]+)')
# TODO(jonathanmetzman): Enforce this.
DEFAULT_RELATIVE_BUILD_INTEGRATION_PATH = '.cifuzz'
# pylint: disable=too-many-lines
class Project:
"""Class representing a project that is in OSS-Fuzz or an external project
(ClusterFuzzLite user)."""
def __init__(self,
project_name_or_path,
is_external=False,
build_integration_path=DEFAULT_RELATIVE_BUILD_INTEGRATION_PATH):
self.is_external = is_external
if self.is_external:
self.name = os.path.basename(os.path.abspath(project_name_or_path))
self.path = project_name_or_path
self.build_integration_path = os.path.join(self.path,
build_integration_path)
else:
self.name = project_name_or_path
self.path = os.path.join(OSS_FUZZ_DIR, 'projects', self.name)
self.build_integration_path = self.path
@property
def dockerfile_path(self):
"""Returns path to the project Dockerfile."""
return os.path.join(self.path, 'Dockerfile')
@property
def language(self):
"""Returns project language."""
if self.is_external:
# TODO(metzman): Handle this properly.
return 'c++'
project_yaml_path = os.path.join(self.path, 'project.yaml')
with open(project_yaml_path) as file_handle:
content = file_handle.read()
for line in content.splitlines():
match = PROJECT_LANGUAGE_REGEX.match(line)
if match:
return match.group(1)
logging.warning('Language not specified in project.yaml.')
return None
@property
def out(self):
"""Returns the out dir for the project. Creates it if needed."""
return _get_out_dir(self.name)
@property
def work(self):
"""Returns the out dir for the project. Creates it if needed."""
return _get_project_build_subdir(self.name, 'work')
@property
def corpus(self):
"""Returns the out dir for the project. Creates it if needed."""
return _get_project_build_subdir(self.name, 'corpus')
def main(): # pylint: disable=too-many-branches,too-many-return-statements
"""Gets subcommand from program arguments and does it. Returns 0 on success 1
on error."""
@ -121,10 +182,37 @@ def bool_to_retcode(boolean):
def parse_args(parser, args=None):
"""Parses |args| using |parser| and returns parsed args."""
"""Parses |args| using |parser| and returns parsed args. Also changes
|args.build_integration_path| to have correct default behavior."""
# Use default argument None for args so that in production, argparse does its
# normal behavior, but unittesting is easier.
return parser.parse_args(args)
parsed_args = parser.parse_args(args)
project = getattr(parsed_args, 'project', None)
if not project:
return parsed_args
# Use hacky method for extracting attributes so that ShellTest works.
# TODO(metzman): Fix this.
is_external = getattr(parsed_args, 'is_external', False)
build_integration_path = getattr(parsed_args, 'build_integration_path', False)
parsed_args.project = Project(parsed_args.project, is_external,
build_integration_path)
return parsed_args
def _add_external_project_args(parser):
parser.add_argument('--build-integration-path',
help=('Path to the build integration for non-OSS-Fuzz '
'projects.'),
default=DEFAULT_RELATIVE_BUILD_INTEGRATION_PATH)
parser.add_argument(
'--external',
help='Is project external?',
default=False,
action='store_true',
)
def get_parser(): # pylint: disable=too-many-statements
@ -134,11 +222,12 @@ def get_parser(): # pylint: disable=too-many-statements
generate_parser = subparsers.add_parser(
'generate', help='Generate files for new project.')
generate_parser.add_argument('project_name')
generate_parser.add_argument('project')
_add_external_project_args(generate_parser)
build_image_parser = subparsers.add_parser('build_image',
help='Build an image.')
build_image_parser.add_argument('project_name')
build_image_parser.add_argument('project')
build_image_parser.add_argument('--pull',
action='store_true',
help='Pull latest base image.')
@ -149,6 +238,7 @@ def get_parser(): # pylint: disable=too-many-statements
build_image_parser.add_argument('--no-pull',
action='store_true',
help='Do not pull latest base image.')
_add_external_project_args(build_image_parser)
build_fuzzers_parser = subparsers.add_parser(
'build_fuzzers', help='Build fuzzers for a project.')
@ -156,7 +246,8 @@ def get_parser(): # pylint: disable=too-many-statements
_add_engine_args(build_fuzzers_parser)
_add_sanitizer_args(build_fuzzers_parser)
_add_environment_args(build_fuzzers_parser)
build_fuzzers_parser.add_argument('project_name')
_add_external_project_args(build_fuzzers_parser)
build_fuzzers_parser.add_argument('project')
build_fuzzers_parser.add_argument('source_path',
help='path of local source',
nargs='?')
@ -185,19 +276,23 @@ def get_parser(): # pylint: disable=too-many-statements
check_build_parser,
choices=['address', 'memory', 'undefined', 'dataflow', 'thread'])
_add_environment_args(check_build_parser)
check_build_parser.add_argument('project_name', help='name of the project')
check_build_parser.add_argument('project',
help='name of the project or path (external)')
check_build_parser.add_argument('fuzzer_name',
help='name of the fuzzer',
nargs='?')
_add_external_project_args(check_build_parser)
run_fuzzer_parser = subparsers.add_parser(
'run_fuzzer', help='Run a fuzzer in the emulated fuzzing environment.')
_add_engine_args(run_fuzzer_parser)
_add_sanitizer_args(run_fuzzer_parser)
_add_environment_args(run_fuzzer_parser)
_add_external_project_args(run_fuzzer_parser)
run_fuzzer_parser.add_argument(
'--corpus-dir', help='directory to store corpus for the fuzz target')
run_fuzzer_parser.add_argument('project_name', help='name of the project')
run_fuzzer_parser.add_argument('project',
help='name of the project or path (external)')
run_fuzzer_parser.add_argument('fuzzer_name', help='name of the fuzzer')
run_fuzzer_parser.add_argument('fuzzer_args',
help='arguments to pass to the fuzzer',
@ -221,35 +316,40 @@ def get_parser(): # pylint: disable=too-many-statements
coverage_parser.add_argument('--corpus-dir',
help='specify location of corpus'
' to be used (requires --fuzz-target argument)')
coverage_parser.add_argument('project_name', help='name of the project')
coverage_parser.add_argument('project',
help='name of the project or path (external)')
coverage_parser.add_argument('extra_args',
help='additional arguments to '
'pass to llvm-cov utility.',
nargs='*')
_add_external_project_args(coverage_parser)
download_corpora_parser = subparsers.add_parser(
'download_corpora', help='Download all corpora for a project.')
download_corpora_parser.add_argument('--fuzz-target',
help='specify name of a fuzz target')
download_corpora_parser.add_argument('project_name',
help='name of the project')
download_corpora_parser.add_argument(
'project', help='name of the project or path (external)')
reproduce_parser = subparsers.add_parser('reproduce',
help='Reproduce a crash.')
reproduce_parser.add_argument('--valgrind',
action='store_true',
help='run with valgrind')
reproduce_parser.add_argument('project_name', help='name of the project')
reproduce_parser.add_argument('project',
help='name of the project or path (external)')
reproduce_parser.add_argument('fuzzer_name', help='name of the fuzzer')
reproduce_parser.add_argument('testcase_path', help='path of local testcase')
reproduce_parser.add_argument('fuzzer_args',
help='arguments to pass to the fuzzer',
nargs=argparse.REMAINDER)
_add_environment_args(reproduce_parser)
_add_external_project_args(reproduce_parser)
shell_parser = subparsers.add_parser(
'shell', help='Run /bin/bash within the builder container.')
shell_parser.add_argument('project_name', help='name of the project')
shell_parser.add_argument('project',
help='name of the project or path (external)')
shell_parser.add_argument('source_path',
help='path of local source',
nargs='?')
@ -257,6 +357,7 @@ def get_parser(): # pylint: disable=too-many-statements
_add_engine_args(shell_parser)
_add_sanitizer_args(shell_parser)
_add_environment_args(shell_parser)
_add_external_project_args(shell_parser)
subparsers.add_parser('pull_images', help='Pull base images.')
return parser
@ -267,19 +368,19 @@ def is_base_image(image_name):
return os.path.exists(os.path.join('infra', 'base-images', image_name))
def check_project_exists(project_name):
def check_project_exists(project):
"""Checks if a project exists."""
if not os.path.exists(_get_project_dir(project_name)):
logging.error('%s does not exist.', project_name)
if not os.path.exists(project.path):
logging.error('%s does not exist.', project.name)
return False
return True
def _check_fuzzer_exists(project_name, fuzzer_name):
def _check_fuzzer_exists(project, fuzzer_name):
"""Checks if a fuzzer exists."""
command = ['docker', 'run', '--rm']
command.extend(['-v', '%s:/out' % _get_out_dir(project_name)])
command.extend(['-v', '%s:/out' % project.out])
command.append('ubuntu:16.04')
command.extend(['/bin/bash', '-c', 'test -f /out/%s' % fuzzer_name])
@ -304,56 +405,20 @@ def _get_command_string(command):
return ' '.join(pipes.quote(part) for part in command)
def _get_project_dir(project_name):
"""Returns path to the project."""
return os.path.join(OSS_FUZZ_DIR, 'projects', project_name)
def get_dockerfile_path(project_name):
"""Returns path to the project Dockerfile."""
return os.path.join(_get_project_dir(project_name), 'Dockerfile')
def _get_project_build_subdir(project_name, subdir_name):
"""Creates the |subdir_name| subdirectory of the |project_name| subdirectory
in |BUILD_DIR| and returns its path."""
directory = os.path.join(BUILD_DIR, subdir_name, project_name)
def _get_project_build_subdir(project, subdir_name):
"""Creates the |subdir_name| subdirectory of the |project| subdirectory in
|BUILD_DIR| and returns its path."""
directory = os.path.join(BUILD_DIR, subdir_name, project)
if not os.path.exists(directory):
os.makedirs(directory)
return directory
def _get_corpus_dir(project_name=''):
"""Creates and returns path to /corpus directory for the given project (if
specified)."""
return _get_project_build_subdir(project_name, 'corpus')
def _get_out_dir(project_name=''):
def _get_out_dir(project=''):
"""Creates and returns path to /out directory for the given project (if
specified)."""
return _get_project_build_subdir(project_name, 'out')
def _get_work_dir(project_name=''):
"""Creates and returns path to /work directory for the given project (if
specified)."""
return _get_project_build_subdir(project_name, 'work')
def _get_project_language(project_name):
"""Returns project language."""
project_yaml_path = os.path.join(OSS_FUZZ_DIR, 'projects', project_name,
'project.yaml')
with open(project_yaml_path) as file_handle:
content = file_handle.read()
for line in content.splitlines():
match = PROJECT_LANGUAGE_REGEX.match(line)
if match:
return match.group(1)
return None
return _get_project_build_subdir(project, 'out')
def _add_architecture_args(parser, choices=('x86_64', 'i386')):
@ -386,17 +451,27 @@ def _add_environment_args(parser):
help="set environment variable e.g. VAR=value")
def build_image_impl(image_name, cache=True, pull=False):
def build_image_impl(project, cache=True, pull=False):
"""Builds image."""
proj_is_base_image = is_base_image(image_name)
if proj_is_base_image:
image_name = project.name
if is_base_image(image_name):
image_project = 'oss-fuzz-base'
dockerfile_dir = os.path.join('infra', 'base-images', image_name)
else:
docker_build_dir = os.path.join(OSS_FUZZ_DIR, 'infra', 'base-images',
image_name)
docker_file_path = None
elif project.is_external:
# External projects need to use the repo root as the build directory.
docker_file_path = os.path.join(project.build_integration_path,
'Dockerfile')
docker_build_dir = project.path
image_project = 'oss-fuzz'
if not check_project_exists(image_name):
else:
if not check_project_exists(project):
return False
dockerfile_dir = os.path.join('projects', image_name)
docker_file_path = None
docker_build_dir = project.path
image_project = 'oss-fuzz'
if pull and not pull_images():
return False
@ -405,9 +480,13 @@ def build_image_impl(image_name, cache=True, pull=False):
if not cache:
build_args.append('--no-cache')
build_args += [
'-t', 'gcr.io/%s/%s' % (image_project, image_name), dockerfile_dir
]
build_args += ['-t', 'gcr.io/%s/%s' % (image_project, image_name)]
if docker_file_path:
build_args += [
'--file',
docker_file_path,
]
build_args.append(docker_build_dir)
return docker_build(build_args)
@ -432,14 +511,12 @@ def workdir_from_lines(lines, default='/src'):
return default
def _workdir_from_dockerfile(project_name):
def _workdir_from_dockerfile(project):
"""Parses WORKDIR from the Dockerfile for the given project."""
dockerfile_path = get_dockerfile_path(project_name)
with open(dockerfile_path) as file_handle:
with open(project.dockerfile_path) as file_handle:
lines = file_handle.readlines()
return workdir_from_lines(lines, default=os.path.join('/src', project_name))
return workdir_from_lines(lines, default=os.path.join('/src', project.name))
def docker_run(run_args, print_output=True):
@ -514,14 +591,14 @@ def build_image(args):
logging.error('Using cached base images...')
# If build_image is called explicitly, don't use cache.
if build_image_impl(args.project_name, cache=args.cache, pull=pull):
if build_image_impl(args.project, cache=args.cache, pull=pull):
return True
return False
def build_fuzzers_impl( # pylint: disable=too-many-arguments,too-many-locals,too-many-branches
project_name,
project,
clean,
engine,
sanitizer,
@ -530,29 +607,23 @@ def build_fuzzers_impl( # pylint: disable=too-many-arguments,too-many-locals,to
source_path,
mount_path=None):
"""Builds fuzzers."""
if not build_image_impl(project_name):
if not build_image_impl(project):
return False
project_out_dir = _get_out_dir(project_name)
project_work_dir = _get_work_dir(project_name)
project_language = _get_project_language(project_name)
if not project_language:
logging.warning('Language not specified in project.yaml. Build may fail.')
if clean:
logging.info('Cleaning existing build artifacts.')
# Clean old and possibly conflicting artifacts in project's out directory.
docker_run([
'-v',
'%s:/out' % project_out_dir, '-t',
'gcr.io/oss-fuzz/%s' % project_name, '/bin/bash', '-c', 'rm -rf /out/*'
'%s:/out' % project.out, '-t',
'gcr.io/oss-fuzz/%s' % project.name, '/bin/bash', '-c', 'rm -rf /out/*'
])
docker_run([
'-v',
'%s:/work' % project_work_dir, '-t',
'gcr.io/oss-fuzz/%s' % project_name, '/bin/bash', '-c', 'rm -rf /work/*'
'%s:/work' % project.work, '-t',
'gcr.io/oss-fuzz/%s' % project.name, '/bin/bash', '-c', 'rm -rf /work/*'
])
else:
@ -565,8 +636,8 @@ def build_fuzzers_impl( # pylint: disable=too-many-arguments,too-many-locals,to
_add_oss_fuzz_ci_if_needed(env)
if project_language:
env.append('FUZZING_LANGUAGE=' + project_language)
if project.language:
env.append('FUZZING_LANGUAGE=' + project.language)
if env_to_add:
env += env_to_add
@ -575,14 +646,14 @@ def build_fuzzers_impl( # pylint: disable=too-many-arguments,too-many-locals,to
if sanitizer == 'memory':
docker_run([
'-v',
'%s:/work' % project_work_dir, 'gcr.io/oss-fuzz-base/msan-libs-builder',
'%s:/work' % project.work, 'gcr.io/oss-fuzz-base/msan-libs-builder',
'bash', '-c', 'cp -r /msan /work'
])
env.append('MSAN_LIBS_PATH=' + '/work/msan')
command = ['--cap-add', 'SYS_PTRACE'] + _env_to_docker_args(env)
if source_path:
workdir = _workdir_from_dockerfile(project_name)
workdir = _workdir_from_dockerfile(project)
if mount_path:
command += [
'-v',
@ -600,9 +671,9 @@ def build_fuzzers_impl( # pylint: disable=too-many-arguments,too-many-locals,to
command += [
'-v',
'%s:/out' % project_out_dir, '-v',
'%s:/work' % project_work_dir, '-t',
'gcr.io/oss-fuzz/%s' % project_name
'%s:/out' % project.out, '-v',
'%s:/work' % project.work, '-t',
'gcr.io/oss-fuzz/%s' % project.name
]
result = docker_run(command)
@ -612,21 +683,19 @@ def build_fuzzers_impl( # pylint: disable=too-many-arguments,too-many-locals,to
# Patch MSan builds to use instrumented shared libraries.
if sanitizer == 'memory':
docker_run([
'-v',
'%s:/out' % project_out_dir, '-v',
'%s:/work' % project_work_dir
] + _env_to_docker_args(env) + [
'gcr.io/oss-fuzz-base/base-sanitizer-libs-builder', 'patch_build.py',
'/out'
])
docker_run(
['-v', '%s:/out' % project.out, '-v',
'%s:/work' % project.work] + _env_to_docker_args(env) + [
'gcr.io/oss-fuzz-base/base-sanitizer-libs-builder',
'patch_build.py', '/out'
])
return True
def build_fuzzers(args):
"""Builds fuzzers."""
return build_fuzzers_impl(args.project_name,
return build_fuzzers_impl(args.project,
args.clean,
args.engine,
args.sanitizer,
@ -645,15 +714,15 @@ def _add_oss_fuzz_ci_if_needed(env):
def check_build(args):
"""Checks that fuzzers in the container execute without errors."""
if not check_project_exists(args.project_name):
if not check_project_exists(args.project):
return False
if (args.fuzzer_name and
not _check_fuzzer_exists(args.project_name, args.fuzzer_name)):
not _check_fuzzer_exists(args.project, args.fuzzer_name)):
return False
fuzzing_language = _get_project_language(args.project_name)
if fuzzing_language is None:
fuzzing_language = args.project.language
if not fuzzing_language:
logging.warning(
'Language not specified in project.yaml. Defaulting to C++.')
fuzzing_language = 'c++'
@ -670,8 +739,7 @@ def check_build(args):
run_args = _env_to_docker_args(env) + [
'-v',
'%s:/out' % _get_out_dir(args.project_name), '-t',
'gcr.io/oss-fuzz-base/base-runner'
'%s:/out' % args.project.out, '-t', 'gcr.io/oss-fuzz-base/base-runner'
]
if args.fuzzer_name:
@ -688,10 +756,10 @@ def check_build(args):
return result
def _get_fuzz_targets(project_name):
def _get_fuzz_targets(project):
"""Returns names of fuzz targest build in the project's /out directory."""
fuzz_targets = []
for name in os.listdir(_get_out_dir(project_name)):
for name in os.listdir(project.out):
if name.startswith('afl-'):
continue
if name.startswith('jazzer_'):
@ -699,7 +767,7 @@ def _get_fuzz_targets(project_name):
if name == 'llvm-symbolizer':
continue
path = os.path.join(_get_out_dir(project_name), name)
path = os.path.join(project.out, name)
# Python and JVM fuzz targets are only executable for the root user, so
# we can't use os.access.
if os.path.isfile(path) and (os.stat(path).st_mode & 0o111):
@ -750,7 +818,7 @@ def _get_latest_corpus(project_name, fuzz_target, base_corpus_dir):
def download_corpora(args):
"""Downloads most recent corpora from GCS for the given project."""
if not check_project_exists(args.project_name):
if not check_project_exists(args.project):
return False
try:
@ -764,22 +832,20 @@ def download_corpora(args):
if args.fuzz_target:
fuzz_targets = [args.fuzz_target]
else:
fuzz_targets = _get_fuzz_targets(args.project_name)
fuzz_targets = _get_fuzz_targets(args.project)
corpus_dir = _get_corpus_dir(args.project_name)
if not os.path.exists(corpus_dir):
os.makedirs(corpus_dir)
corpus_dir = args.project.corpus
def _download_for_single_target(fuzz_target):
try:
_get_latest_corpus(args.project_name, fuzz_target, corpus_dir)
_get_latest_corpus(args.project, fuzz_target, corpus_dir)
return True
except Exception as error: # pylint:disable=broad-except
logging.error('Corpus download for %s failed: %s.', fuzz_target,
str(error))
return False
logging.info('Downloading corpora for %s project to %s.', args.project_name,
logging.info('Downloading corpora for %s project to %s.', args.project.name,
corpus_dir)
thread_pool = ThreadPool()
return all(thread_pool.map(_download_for_single_target, fuzz_targets))
@ -793,24 +859,24 @@ def coverage(args):
'--fuzz-target')
return False
if not check_project_exists(args.project_name):
if not check_project_exists(args.project):
return False
project_language = _get_project_language(args.project_name)
if project_language not in LANGUAGES_WITH_COVERAGE_SUPPORT:
if args.project.language not in LANGUAGES_WITH_COVERAGE_SUPPORT:
logging.error(
'Project is written in %s, coverage for it is not supported yet.',
project_language)
args.project.language)
return False
if not args.no_corpus_download and not args.corpus_dir:
if (not args.no_corpus_download and not args.corpus_dir and
not args.project.is_external):
if not download_corpora(args):
return False
env = [
'FUZZING_ENGINE=libfuzzer',
'FUZZING_LANGUAGE=%s' % project_language,
'PROJECT=%s' % args.project_name,
'FUZZING_LANGUAGE=%s' % args.project.language,
'PROJECT=%s' % args.project.name,
'SANITIZER=coverage',
'HTTP_PORT=%s' % args.port,
'COVERAGE_EXTRA_ARGS=%s' % ' '.join(args.extra_args),
@ -832,11 +898,11 @@ def coverage(args):
corpus_dir = os.path.realpath(args.corpus_dir)
run_args.extend(['-v', '%s:/corpus/%s' % (corpus_dir, args.fuzz_target)])
else:
run_args.extend(['-v', '%s:/corpus' % _get_corpus_dir(args.project_name)])
run_args.extend(['-v', '%s:/corpus' % args.project.corpus])
run_args.extend([
'-v',
'%s:/out' % _get_out_dir(args.project_name),
'%s:/out' % args.project.out,
'-t',
'gcr.io/oss-fuzz-base/base-runner',
])
@ -856,10 +922,10 @@ def coverage(args):
def run_fuzzer(args):
"""Runs a fuzzer in the container."""
if not check_project_exists(args.project_name):
if not check_project_exists(args.project):
return False
if not _check_fuzzer_exists(args.project_name, args.fuzzer_name):
if not _check_fuzzer_exists(args.project, args.fuzzer_name):
return False
env = [
@ -886,7 +952,7 @@ def run_fuzzer(args):
run_args.extend([
'-v',
'%s:/out' % _get_out_dir(args.project_name),
'%s:/out' % args.project.out,
'-t',
'gcr.io/oss-fuzz-base/base-runner',
'run_fuzzer',
@ -898,12 +964,12 @@ def run_fuzzer(args):
def reproduce(args):
"""Reproduces a specific test case from a specific project."""
return reproduce_impl(args.project_name, args.fuzzer_name, args.valgrind,
args.e, args.fuzzer_args, args.testcase_path)
return reproduce_impl(args.project, args.fuzzer_name, args.valgrind, args.e,
args.fuzzer_args, args.testcase_path)
def reproduce_impl( # pylint: disable=too-many-arguments
project_name,
project,
fuzzer_name,
valgrind,
env_to_add,
@ -912,10 +978,10 @@ def reproduce_impl( # pylint: disable=too-many-arguments
run_function=docker_run,
err_result=False):
"""Reproduces a testcase in the container."""
if not check_project_exists(project_name):
if not check_project_exists(project):
return err_result
if not _check_fuzzer_exists(project_name, fuzzer_name):
if not _check_fuzzer_exists(project, fuzzer_name):
return err_result
debugger = ''
@ -934,7 +1000,7 @@ def reproduce_impl( # pylint: disable=too-many-arguments
run_args = _env_to_docker_args(env) + [
'-v',
'%s:/out' % _get_out_dir(project_name),
'%s:/out' % project.out,
'-v',
'%s:/testcase' % _get_absolute_path(testcase_path),
'-t',
@ -947,51 +1013,85 @@ def reproduce_impl( # pylint: disable=too-many-arguments
return run_function(run_args)
def generate(args):
"""Generates empty project files."""
if len(args.project_name) > MAX_PROJECT_NAME_LENGTH:
def _validate_project_name(project_name):
"""Validates |project_name| is a valid OSS-Fuzz project name."""
if len(project_name) > MAX_PROJECT_NAME_LENGTH:
logging.error(
'Project name needs to be less than or equal to %d characters.',
MAX_PROJECT_NAME_LENGTH)
return False
if not VALID_PROJECT_NAME_REGEX.match(args.project_name):
logging.error('Invalid project name.')
if not VALID_PROJECT_NAME_REGEX.match(project_name):
logging.info('Invalid project name: %s.', project_name)
return False
directory = os.path.join('projects', args.project_name)
return True
def _create_build_integration_directory(directory):
"""Returns True on successful creation of a build integration directory.
Suitable for OSS-Fuzz and external projects."""
try:
os.mkdir(directory)
os.makedirs(directory)
except OSError as error:
if error.errno != errno.EEXIST:
raise
logging.error('%s already exists.', directory)
return False
return True
logging.info('Writing new files to %s.', directory)
def _template_project_file(filename, template, template_args, directory):
"""Templates |template| using |template_args| and writes the result to
|directory|/|filename|. Sets the file to executable if |filename| is
build.sh."""
file_path = os.path.join(directory, filename)
with open(file_path, 'w') as file_handle:
file_handle.write(template % template_args)
if filename == 'build.sh':
os.chmod(file_path, 0o755)
def generate(args):
"""Generates empty project files."""
return _generate_impl(args.project)
def _get_current_datetime():
"""Returns this year. Needed for mocking."""
return datetime.datetime.now()
def _generate_impl(project):
"""Implementation of generate(). Useful for testing."""
if project.is_external:
# External project.
project_templates = templates.EXTERNAL_TEMPLATES
else:
# Internal project.
if not _validate_project_name(project.name):
return False
project_templates = templates.TEMPLATES
directory = project.build_integration_path
if not _create_build_integration_directory(directory):
return False
logging.info('Writing new files to: %s.', directory)
template_args = {
'project_name': args.project_name,
'year': datetime.datetime.now().year
'project_name': project.name,
'year': _get_current_datetime().year
}
with open(os.path.join(directory, 'project.yaml'), 'w') as file_handle:
file_handle.write(templates.PROJECT_YAML_TEMPLATE % template_args)
with open(os.path.join(directory, 'Dockerfile'), 'w') as file_handle:
file_handle.write(templates.DOCKER_TEMPLATE % template_args)
build_sh_path = os.path.join(directory, 'build.sh')
with open(build_sh_path, 'w') as file_handle:
file_handle.write(templates.BUILD_TEMPLATE % template_args)
os.chmod(build_sh_path, 0o755)
for filename, template in project_templates.items():
_template_project_file(filename, template, template_args, directory)
return True
def shell(args):
"""Runs a shell within a docker image."""
if not build_image_impl(args.project_name):
if not build_image_impl(args.project):
return False
env = [
@ -1000,18 +1100,18 @@ def shell(args):
'ARCHITECTURE=' + args.architecture,
]
if args.project_name != 'base-runner-debug':
env.append('FUZZING_LANGUAGE=' + _get_project_language(args.project_name))
if args.project.name != 'base-runner-debug':
env.append('FUZZING_LANGUAGE=' + args.project.language)
if args.e:
env += args.e
if is_base_image(args.project_name):
if is_base_image(args.project.name):
image_project = 'oss-fuzz-base'
out_dir = _get_out_dir()
else:
image_project = 'oss-fuzz'
out_dir = _get_out_dir(args.project_name)
out_dir = args.project.out
run_args = _env_to_docker_args(env)
if args.source_path:
@ -1023,8 +1123,8 @@ def shell(args):
run_args.extend([
'-v',
'%s:/out' % out_dir, '-v',
'%s:/work' % _get_work_dir(args.project_name), '-t',
'gcr.io/%s/%s' % (image_project, args.project_name), '/bin/bash'
'%s:/work' % args.project.work, '-t',
'gcr.io/%s/%s' % (image_project, args.project.name), '/bin/bash'
])
docker_run(run_args)

View File

@ -13,18 +13,25 @@
# limitations under the License.
"""Tests for helper.py"""
import datetime
import os
import unittest
from unittest import mock
from pyfakefs import fake_filesystem_unittest
import helper
import templates
# pylint: disable=no-self-use,protected-access
class TestShell(unittest.TestCase):
class ShellTest(unittest.TestCase):
"""Tests 'shell' command."""
@mock.patch('helper.docker_run')
@mock.patch('helper.build_image_impl')
def test_base_runner_debug(self, mocked_build_image_impl, _):
def test_base_runner_debug(self, _, __):
"""Tests that shell base-runner-debug works as intended."""
image_name = 'base-runner-debug'
unparsed_args = ['shell', image_name]
@ -32,5 +39,95 @@ class TestShell(unittest.TestCase):
args = helper.parse_args(parser, unparsed_args)
args.sanitizer = 'address'
result = helper.shell(args)
mocked_build_image_impl.assert_called_with(image_name)
self.assertTrue(result)
class BuildImageImplTest(unittest.TestCase):
"""Tests for build_image_impl."""
@mock.patch('helper.docker_build')
def test_no_cache(self, mocked_docker_build):
"""Tests that cache=False is handled properly."""
image_name = 'base-image'
helper.build_image_impl(helper.Project(image_name), cache=False)
self.assertIn('--no-cache', mocked_docker_build.call_args_list[0][0][0])
@mock.patch('helper.docker_build')
@mock.patch('helper.pull_images')
def test_pull(self, mocked_pull_images, _):
"""Tests that pull=True is handled properly."""
image_name = 'base-image'
helper.build_image_impl(helper.Project(image_name), pull=True)
mocked_pull_images.assert_called_with()
@mock.patch('helper.docker_build')
def test_base_image(self, mocked_docker_build):
"""Tests that build_image_impl works as intended with a base-image."""
image_name = 'base-image'
helper.build_image_impl(helper.Project(image_name))
mocked_docker_build.assert_called_with([
'-t', 'gcr.io/oss-fuzz-base/base-image',
os.path.join(helper.OSS_FUZZ_DIR, 'infra/base-images/base-image')
])
@mock.patch('helper.docker_build')
def test_oss_fuzz_project(self, mocked_docker_build):
"""Tests that build_image_impl works as intended with an OSS-Fuzz
project."""
project_name = 'example'
helper.build_image_impl(helper.Project(project_name))
mocked_docker_build.assert_called_with([
'-t', 'gcr.io/oss-fuzz/example',
os.path.join(helper.OSS_FUZZ_DIR, 'projects/example')
])
@mock.patch('helper.docker_build')
def test_external_project(self, mocked_docker_build):
"""Tests that build_image_impl works as intended with a non-OSS-Fuzz
project."""
project_src_path = '/example'
build_integration_path = 'build-integration'
project = helper.Project(project_src_path,
is_external=True,
build_integration_path=build_integration_path)
helper.build_image_impl(project)
mocked_docker_build.assert_called_with([
'-t', 'gcr.io/oss-fuzz/example', '--file',
'/example/build-integration/Dockerfile', project_src_path
])
class GenerateImplTest(fake_filesystem_unittest.TestCase):
"""Tests for _generate_impl."""
PROJECT_NAME = 'newfakeproject'
def setUp(self):
self.setUpPyfakefs()
self.fs.add_real_directory(helper.OSS_FUZZ_DIR)
def _verify_templated_files(self, template_dict, directory):
template_args = {'project_name': self.PROJECT_NAME, 'year': 2021}
for filename, template in template_dict.items():
file_path = os.path.join(directory, filename)
with open(file_path, 'r') as file_handle:
contents = file_handle.read()
self.assertEqual(contents, template % template_args)
@mock.patch('helper._get_current_datetime',
return_value=datetime.datetime(year=2021, month=1, day=1))
def test_generate_oss_fuzz_project(self, _):
"""Tests that the correct files are generated for an OSS-Fuzz project."""
helper._generate_impl(helper.Project(self.PROJECT_NAME))
self._verify_templated_files(
templates.TEMPLATES,
os.path.join(helper.OSS_FUZZ_DIR, 'projects', self.PROJECT_NAME))
def test_generate_external_project(self):
"""Tests that the correct files are generated for a non-OSS-Fuzz project."""
build_integration_path = '/newfakeproject/build-integration'
helper._generate_impl(
helper.Project('/newfakeproject/',
is_external=True,
build_integration_path=build_integration_path))
self._verify_templated_files(templates.EXTERNAL_TEMPLATES,
build_integration_path)

View File

@ -46,6 +46,14 @@ WORKDIR %(project_name)s
COPY build.sh $SRC/
"""
EXTERNAL_DOCKER_TEMPLATE = """\
FROM gcr.io/oss-fuzz-base/base-builder
RUN apt-get update && apt-get install -y make autoconf automake libtool
RUN git clone --depth 1 <git_url> %(project_name)s # or use other version control
WORKDIR %(project_name)s
COPY build.sh $SRC/
"""
BUILD_TEMPLATE = """\
#!/bin/bash -eu
# Copyright %(year)d Google LLC
@ -76,3 +84,28 @@ BUILD_TEMPLATE = """\
# /path/to/name_of_fuzzer.cc -o $OUT/name_of_fuzzer \\
# $LIB_FUZZING_ENGINE /path/to/library.a
"""
EXTERNAL_BUILD_TEMPLATE = """\
# build project
# e.g.
# ./autogen.sh
# ./configure
# make -j$(nproc) all
# build fuzzers
# e.g.
# $CXX $CXXFLAGS -std=c++11 -Iinclude \\
# /path/to/name_of_fuzzer.cc -o $OUT/name_of_fuzzer \\
# $LIB_FUZZING_ENGINE /path/to/library.a
"""
TEMPLATES = {
'build.sh': BUILD_TEMPLATE,
'Dockerfile': DOCKER_TEMPLATE,
'project.yaml': PROJECT_YAML_TEMPLATE
}
EXTERNAL_TEMPLATES = {
'build.sh': EXTERNAL_BUILD_TEMPLATE,
'Dockerfile': EXTERNAL_DOCKER_TEMPLATE
}