def bisect(bisect_type, old_commit, new_commit, test_case_path, fuzz_target, build_data): """From a commit range, this function caluclates which introduced a specific error from a fuzz test_case_path. Args: bisect_type: The type of the bisect ('regressed' or 'fixed'). old_commit: The oldest commit in the error regression range. new_commit: The newest commit in the error regression range. test_case_path: The file path of the test case that triggers the error fuzz_target: The name of the fuzzer to be tested. build_data: a class holding all of the input parameters for bisection. Returns: The commit SHA that introduced the error or None. Raises: ValueError: when a repo url can't be determine from the project. """ try: return _bisect(bisect_type, old_commit, new_commit, test_case_path, fuzz_target, build_data) finally: # Clean up projects/ as _bisect may have modified it. oss_fuzz_repo_manager = repo_manager.BaseRepoManager( helper.OSS_FUZZ_DIR) oss_fuzz_repo_manager.git(['reset', 'projects']) oss_fuzz_repo_manager.git(['checkout', 'projects']) oss_fuzz_repo_manager.git(['clean', '-fxd', 'projects'])
def build_fuzzers_from_commit(commit, build_repo_manager, host_src_path, build_data): """Builds a OSS-Fuzz fuzzer at a specific commit SHA. Args: commit: The commit SHA to build the fuzzers at. build_repo_manager: The OSS-Fuzz project's repo manager to be built at. build_data: A struct containing project build information. Returns: 0 on successful build or error code on failure. """ oss_fuzz_repo_manager = repo_manager.BaseRepoManager(helper.OSS_FUZZ_DIR) num_retry = 1 for i in range(num_retry + 1): build_repo_manager.checkout_commit(commit, clean=False) result = helper.build_fuzzers_impl(project_name=build_data.project_name, clean=True, engine=build_data.engine, sanitizer=build_data.sanitizer, architecture=build_data.architecture, env_to_add=None, source_path=host_src_path, mount_location='/src') if result == 0 or i == num_retry: break # Retry with an OSS-Fuzz builder container that's closer to the project # commit date. commit_date = build_repo_manager.commit_date(commit) projects_dir = os.path.join('projects', build_data.project_name) # Find first change in the projects/<PROJECT> directory before the project # commit date. oss_fuzz_commit, _, _ = oss_fuzz_repo_manager.git([ 'log', '--before=' + commit_date.isoformat(), '-n1', '--format=%H', projects_dir ], check_result=True) oss_fuzz_commit = oss_fuzz_commit.strip() if not oss_fuzz_commit: logging.warning('No suitable earlier OSS-Fuzz commit found.') break logging.info('Build failed. Retrying on earlier OSS-Fuzz commit %s.', oss_fuzz_commit) # Check out projects/<PROJECT> dir to the commit that was found. oss_fuzz_repo_manager.git(['checkout', oss_fuzz_commit, projects_dir], check_result=True) # Rebuild image and re-copy src dir since things in /src could have changed. if not helper.build_image_impl(build_data.project_name): raise RuntimeError('Failed to rebuild image.') shutil.rmtree(host_src_path, ignore_errors=True) copy_src_from_docker(build_data.project_name, os.path.dirname(host_src_path)) return result == 0
def _bisect(old_commit, new_commit, test_case_path, fuzz_target, build_data): # pylint: disable=too-many-locals """Perform the bisect.""" with tempfile.TemporaryDirectory() as tmp_dir: repo_url, repo_path = build_specified_commit.detect_main_repo( build_data.project_name, commit=new_commit) if not repo_url or not repo_path: raise ValueError('Main git repo can not be determined.') # Copy /src from the built Docker container to ensure all dependencies # exist. This will be mounted when running them. host_src_dir = build_specified_commit.copy_src_from_docker( build_data.project_name, tmp_dir) bisect_repo_manager = repo_manager.BaseRepoManager( os.path.join(host_src_dir, os.path.basename(repo_path))) commit_list = bisect_repo_manager.get_commit_list(new_commit, old_commit) old_idx = len(commit_list) - 1 new_idx = 0 logging.info('Testing against new_commit (%s)', commit_list[new_idx]) if not build_specified_commit.build_fuzzers_from_commit( commit_list[new_idx], bisect_repo_manager, host_src_dir, build_data): raise RuntimeError('Failed to build new_commit') expected_error_code = helper.reproduce_impl(build_data.project_name, fuzz_target, False, [], [], test_case_path) # Check if the error is persistent through the commit range if old_commit: logging.info('Testing against old_commit (%s)', commit_list[old_idx]) if not build_specified_commit.build_fuzzers_from_commit( commit_list[old_idx], bisect_repo_manager, host_src_dir, build_data, ): raise RuntimeError('Failed to build old_commit') if expected_error_code == helper.reproduce_impl(build_data.project_name, fuzz_target, False, [], [], test_case_path): return Result(repo_url, commit_list[old_idx]) while old_idx - new_idx > 1: curr_idx = (old_idx + new_idx) // 2 logging.info('Testing against %s (idx=%d)', commit_list[curr_idx], curr_idx) build_specified_commit.build_fuzzers_from_commit(commit_list[curr_idx], bisect_repo_manager, host_src_dir, build_data) error_code = helper.reproduce_impl(build_data.project_name, fuzz_target, False, [], [], test_case_path) if expected_error_code == error_code: new_idx = curr_idx else: old_idx = curr_idx return Result(repo_url, commit_list[new_idx])
def main(): """Main function.""" logging.getLogger().setLevel(logging.INFO) parser = argparse.ArgumentParser( description='Build fuzzers at a specific commit') parser.add_argument('--project_name', help='The name of the project where the bug occurred.', required=True) parser.add_argument('--commit', help='The newest commit SHA to be bisected.', required=True) parser.add_argument('--engine', help='The default is "libfuzzer".', default='libfuzzer') parser.add_argument('--sanitizer', default='address', help='The default is "address".') parser.add_argument('--architecture', default='x86_64') args = parser.parse_args() repo_url, repo_path = detect_main_repo(args.project_name, commit=args.commit) if not repo_url or not repo_path: raise ValueError('Main git repo can not be determined.') with tempfile.TemporaryDirectory() as tmp_dir: host_src_dir = copy_src_from_docker(args.project_name, tmp_dir) build_repo_manager = repo_manager.BaseRepoManager( os.path.join(host_src_dir, os.path.basename(repo_path))) base_builder_repo = load_base_builder_repo() build_data = BuildData(project_name=args.project_name, engine=args.engine, sanitizer=args.sanitizer, architecture=args.architecture) if not build_fuzzers_from_commit(args.commit, build_repo_manager, host_src_dir, build_data, base_builder_repo=base_builder_repo): raise RuntimeError('Failed to build.')
def build_fuzzers_from_commit(commit, build_repo_manager, host_src_path, build_data, base_builder_repo=None): """Builds a OSS-Fuzz fuzzer at a specific commit SHA. Args: commit: The commit SHA to build the fuzzers at. build_repo_manager: The OSS-Fuzz project's repo manager to be built at. build_data: A struct containing project build information. base_builder_repo: A BaseBuilderRepo. Returns: 0 on successful build or error code on failure. """ oss_fuzz_repo_manager = repo_manager.BaseRepoManager(helper.OSS_FUZZ_DIR) num_retry = 1 def cleanup(): # Re-copy /src for a clean checkout every time. copy_src_from_docker(build_data.project_name, os.path.dirname(host_src_path)) projects_dir = os.path.join('projects', build_data.project_name) dockerfile_path = os.path.join(projects_dir, 'Dockerfile') for i in range(num_retry + 1): build_repo_manager.checkout_commit(commit, clean=False) post_checkout_steps = get_required_post_checkout_steps(dockerfile_path) for workdir, post_checkout_step in post_checkout_steps: logging.info('Running post-checkout step `%s` in %s.', post_checkout_step, workdir) helper.docker_run([ '-w', workdir, '-v', host_src_path + ':' + '/src', 'gcr.io/oss-fuzz/' + build_data.project_name, '/bin/bash', '-c', post_checkout_step, ]) result = helper.build_fuzzers_impl( project_name=build_data.project_name, clean=True, engine=build_data.engine, sanitizer=build_data.sanitizer, architecture=build_data.architecture, env_to_add=None, source_path=host_src_path, mount_location='/src') if result == 0 or i == num_retry: break # Retry with an OSS-Fuzz builder container that's closer to the project # commit date. commit_date = build_repo_manager.commit_date(commit) # Find first change in the projects/<PROJECT> directory before the project # commit date. oss_fuzz_commit, _, _ = oss_fuzz_repo_manager.git([ 'log', '--before=' + commit_date.isoformat(), '-n1', '--format=%H', projects_dir ], check_result=True) oss_fuzz_commit = oss_fuzz_commit.strip() if not oss_fuzz_commit: logging.warning('No suitable earlier OSS-Fuzz commit found.') break logging.info('Build failed. Retrying on earlier OSS-Fuzz commit %s.', oss_fuzz_commit) # Check out projects/<PROJECT> dir to the commit that was found. oss_fuzz_repo_manager.git(['checkout', oss_fuzz_commit, projects_dir], check_result=True) # Also use the closest base-builder we can find. if base_builder_repo: base_builder_digest = base_builder_repo.find_digest(commit_date) logging.info('Using base-builder with digest %s.', base_builder_digest) _replace_base_builder_digest(dockerfile_path, base_builder_digest) # Rebuild image and re-copy src dir since things in /src could have changed. if not _build_image_with_retries(build_data.project_name): raise RuntimeError('Failed to rebuild image.') cleanup() cleanup() return result == 0
def _bisect(bisect_type, old_commit, new_commit, test_case_path, fuzz_target, build_data): """Perform the bisect.""" # pylint: disable=too-many-branches base_builder_repo = build_specified_commit.load_base_builder_repo() with tempfile.TemporaryDirectory() as tmp_dir: repo_url, repo_path = build_specified_commit.detect_main_repo( build_data.project_name, commit=new_commit) if not repo_url or not repo_path: raise ValueError('Main git repo can not be determined.') if old_commit == new_commit: raise BisectError('old_commit is the same as new_commit', repo_url) # Copy /src from the built Docker container to ensure all dependencies # exist. This will be mounted when running them. host_src_dir = build_specified_commit.copy_src_from_docker( build_data.project_name, tmp_dir) bisect_repo_manager = repo_manager.BaseRepoManager( os.path.join(host_src_dir, os.path.basename(repo_path))) commit_list = bisect_repo_manager.get_commit_list( new_commit, old_commit) old_idx = len(commit_list) - 1 new_idx = 0 logging.info('Testing against new_commit (%s)', commit_list[new_idx]) if not build_specified_commit.build_fuzzers_from_commit( commit_list[new_idx], bisect_repo_manager, host_src_dir, build_data, base_builder_repo=base_builder_repo): raise BisectError('Failed to build new_commit', repo_url) if bisect_type == 'fixed': should_crash = False elif bisect_type == 'regressed': should_crash = True else: raise BisectError('Invalid bisect type ' + bisect_type, repo_url) expected_error = _check_for_crash(build_data.project_name, fuzz_target, test_case_path) logging.info('new_commit result = %s', expected_error) if not should_crash and expected_error: logging.warning('new_commit crashed but not shouldn\'t. ' 'Continuing to see if stack changes.') range_valid = False for _ in range(2): logging.info('Testing against old_commit (%s)', commit_list[old_idx]) if not build_specified_commit.build_fuzzers_from_commit( commit_list[old_idx], bisect_repo_manager, host_src_dir, build_data, base_builder_repo=base_builder_repo): raise BisectError('Failed to build old_commit', repo_url) if _check_for_crash(build_data.project_name, fuzz_target, test_case_path) == expected_error: logging.warning( 'old_commit %s had same result as new_commit %s', old_commit, new_commit) # Try again on an slightly older commit. old_commit = bisect_repo_manager.get_parent(old_commit, 64) if not old_commit: break commit_list = bisect_repo_manager.get_commit_list( new_commit, old_commit) old_idx = len(commit_list) - 1 continue range_valid = True break if not range_valid: raise BisectError('old_commit had same result as new_commit', repo_url) while old_idx - new_idx > 1: curr_idx = (old_idx + new_idx) // 2 logging.info('Testing against %s (idx=%d)', commit_list[curr_idx], curr_idx) if not build_specified_commit.build_fuzzers_from_commit( commit_list[curr_idx], bisect_repo_manager, host_src_dir, build_data, base_builder_repo=base_builder_repo): # Treat build failures as if we couldn't repo. # TODO(ochang): retry nearby commits? old_idx = curr_idx continue current_error = _check_for_crash(build_data.project_name, fuzz_target, test_case_path) logging.info('Current result = %s', current_error) if expected_error == current_error: new_idx = curr_idx else: old_idx = curr_idx return Result(repo_url, commit_list[new_idx])
def _bisect(bisect_type, old_commit, new_commit, test_case_path, fuzz_target, build_data): """Perform the bisect.""" # pylint: disable=too-many-branches base_builder_repo = _load_base_builder_repo() with tempfile.TemporaryDirectory() as tmp_dir: repo_url, repo_path = build_specified_commit.detect_main_repo( build_data.project_name, commit=new_commit) if not repo_url or not repo_path: raise ValueError('Main git repo can not be determined.') # Copy /src from the built Docker container to ensure all dependencies # exist. This will be mounted when running them. host_src_dir = build_specified_commit.copy_src_from_docker( build_data.project_name, tmp_dir) bisect_repo_manager = repo_manager.BaseRepoManager( os.path.join(host_src_dir, os.path.basename(repo_path))) commit_list = bisect_repo_manager.get_commit_list( new_commit, old_commit) old_idx = len(commit_list) - 1 new_idx = 0 logging.info('Testing against new_commit (%s)', commit_list[new_idx]) if not build_specified_commit.build_fuzzers_from_commit( commit_list[new_idx], bisect_repo_manager, host_src_dir, build_data, base_builder_repo=base_builder_repo): raise RuntimeError('Failed to build new_commit') if bisect_type == 'fixed': expected_error = False elif bisect_type == 'regressed': expected_error = True else: raise ValueError('Invalid bisect type ' + bisect_type) if _check_for_crash(build_data.project_name, fuzz_target, test_case_path) != expected_error: raise RuntimeError('new_commit didn\'t have expected result.') # Check if the error is persistent through the commit range if old_commit: logging.info('Testing against old_commit (%s)', commit_list[old_idx]) if not build_specified_commit.build_fuzzers_from_commit( commit_list[old_idx], bisect_repo_manager, host_src_dir, build_data, base_builder_repo=base_builder_repo): raise RuntimeError('Failed to build old_commit') if _check_for_crash(build_data.project_name, fuzz_target, test_case_path) == expected_error: raise RuntimeError('old_commit had same result as new_commit') while old_idx - new_idx > 1: curr_idx = (old_idx + new_idx) // 2 logging.info('Testing against %s (idx=%d)', commit_list[curr_idx], curr_idx) if not build_specified_commit.build_fuzzers_from_commit( commit_list[curr_idx], bisect_repo_manager, host_src_dir, build_data, base_builder_repo=base_builder_repo): # Treat build failures as if we couldn't repo. # TODO(ochang): retry nearby commits? old_idx = curr_idx continue is_error = _check_for_crash(build_data.project_name, fuzz_target, test_case_path) if expected_error == is_error: new_idx = curr_idx else: old_idx = curr_idx return Result(repo_url, commit_list[new_idx])