def test_detect_main_repo_from_name(self): """Test the detect main repo function from build specific commit module.""" repo_origin, repo_name = build_specified_commit.detect_main_repo( 'curl', repo_name='curl') self.assertEqual(repo_origin, 'https://github.com/curl/curl.git') self.assertEqual(repo_name, 'curl') repo_origin, repo_name = build_specified_commit.detect_main_repo( 'yara', repo_name='yara') self.assertEqual(repo_origin, 'https://github.com/VirusTotal/yara.git') self.assertEqual(repo_name, 'yara') repo_origin, repo_name = build_specified_commit.detect_main_repo( 'usrsctp', repo_name='usrsctp') self.assertEqual(repo_origin, 'https://github.com/weinrank/usrsctp') self.assertEqual(repo_name, 'usrsctp') repo_origin, repo_name = build_specified_commit.detect_main_repo( 'ndpi', repo_name='nDPI') self.assertEqual(repo_origin, 'https://github.com/ntop/nDPI.git') self.assertEqual(repo_name, 'ndpi') repo_origin, repo_name = build_specified_commit.detect_main_repo( 'notproj', repo_name='aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa') self.assertIsNone(repo_origin) self.assertIsNone(repo_name)
def test_detect_main_repo_from_commit(self): """Test the detect main repo function from build specific commit module.""" for example_repo in test_repos.TEST_REPOS: repo_origin, repo_name = build_specified_commit.detect_main_repo( example_repo.project_name, commit=example_repo.new_commit) self.assertEqual(repo_origin, example_repo.git_url) self.assertEqual(repo_name, os.path.join('/src', example_repo.oss_repo_name)) repo_origin, repo_name = build_specified_commit.detect_main_repo( test_repos.INVALID_REPO.project_name, test_repos.INVALID_REPO.new_commit) self.assertIsNone(repo_origin) self.assertIsNone(repo_name)
def build_image_and_checkout_src(self): """Builds the project builder image for a non-OSS-Fuzz project. Sets the repo manager and host_repo_path. Checks out source code of project with patch under test. Returns True on success.""" logging.info('Building OSS-Fuzz project on Github Actions.') # detect_main_repo builds the image as a side effect. inferred_url, self.image_repo_path = ( build_specified_commit.detect_main_repo( self.project_name, repo_name=self.project_repo_name)) if not inferred_url or not self.image_repo_path: logging.error('Could not detect repo from project %s.', self.project_name) return False git_workspace = os.path.join(self.workspace, 'storage') os.makedirs(git_workspace, exist_ok=True) # Use the same name used in the docker image so we can overwrite it. image_repo_name = os.path.basename(self.image_repo_path) # Checkout project's repo in the shared volume. self.repo_manager = repo_manager.clone_repo_and_get_manager( inferred_url, git_workspace, repo_name=image_repo_name) self.host_repo_path = self.repo_manager.repo_dir checkout_specified_commit(self.repo_manager, self.pr_ref, self.commit_sha) return True
def prepare_for_fuzzer_build(self): """Builds the fuzzer builder image, checks out the pull request/commit and returns the BuildPreparationResult.""" logging.info('Building OSS-Fuzz project on Github Actions.') assert self.config.pr_ref or self.config.commit_sha # detect_main_repo builds the image as a side effect. inferred_url, image_repo_path = ( build_specified_commit.detect_main_repo( self.config.oss_fuzz_project_name, repo_name=self.config.project_repo_name)) if not inferred_url or not image_repo_path: logging.error('Could not detect repo.') return BuildPreparationResult(success=False, image_repo_path=None, repo_manager=None) os.makedirs(self.workspace.repo_storage, exist_ok=True) # Use the same name used in the docker image so we can overwrite it. image_repo_name = os.path.basename(image_repo_path) # Checkout project's repo in the shared volume. manager = repo_manager.clone_repo_and_get_manager( inferred_url, self.workspace.repo_storage, repo_name=image_repo_name) checkout_specified_commit(manager, self.config.pr_ref, self.config.commit_sha) return BuildPreparationResult(success=True, image_repo_path=image_repo_path, repo_manager=manager)
def bisect(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: 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. """ with tempfile.TemporaryDirectory() as tmp_dir: repo_url, repo_name = build_specified_commit.detect_main_repo( build_data.project_name, commit=old_commit) if not repo_url or not repo_name: raise ValueError('Main git repo can not be determined.') bisect_repo_manager = repo_manager.RepoManager(repo_url, tmp_dir, repo_name=repo_name) commit_list = bisect_repo_manager.get_commit_list( old_commit, new_commit) old_idx = len(commit_list) - 1 new_idx = 0 build_specified_commit.build_fuzzers_from_commit( commit_list[new_idx], bisect_repo_manager, build_data) 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 build_specified_commit.build_fuzzers_from_commit( commit_list[old_idx], bisect_repo_manager, build_data, ) if expected_error_code == helper.reproduce_impl( build_data.project_name, fuzz_target, False, [], [], test_case_path): return commit_list[old_idx] while old_idx - new_idx > 1: curr_idx = (old_idx + new_idx) // 2 build_specified_commit.build_fuzzers_from_commit( commit_list[curr_idx], bisect_repo_manager, 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 commit_list[new_idx]
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 _detect_main_repo(self): """Helper for child classes that detects the main repo and returns a tuple containing the inffered url and path to the repo in the image.""" inferred_url, image_repo_path = build_specified_commit.detect_main_repo( self.config.oss_fuzz_project_name, repo_name=self.config.project_repo_name) if not inferred_url or not image_repo_path: logging.error('Could not detect repo.') return inferred_url, image_repo_path
def test_detect_main_repo_from_name(self): """Test the detect main repo function from build specific commit module.""" for example_repo in test_repos.TEST_REPOS: if example_repo.project_name == 'gonids': # It's unclear how this test ever passed, but we can't infer the repo # because gonids doesn't really check it out, it uses "go get". continue repo_origin, repo_name = build_specified_commit.detect_main_repo( example_repo.project_name, repo_name=example_repo.git_repo_name) self.assertEqual(repo_origin, example_repo.git_url) self.assertEqual( repo_name, os.path.join(example_repo.image_location, example_repo.oss_repo_name)) repo_origin, repo_name = build_specified_commit.detect_main_repo( test_repos.INVALID_REPO.project_name, test_repos.INVALID_REPO.oss_repo_name) self.assertIsNone(repo_origin) self.assertIsNone(repo_name)
def test_detect_main_repo_from_commit(self): """Test the detect main repo function from build specific commit module.""" # TODO(metzman): Fix these tests so they don't randomly break because of # changes in the outside world. for example_repo in test_repos.TEST_REPOS: if example_repo.new_commit: # TODO(metzman): This function calls _build_image_with_retries which # has a long delay (30 seconds). Figure out how to make this quicker. repo_origin, repo_name = build_specified_commit.detect_main_repo( example_repo.project_name, commit=example_repo.new_commit) self.assertEqual(repo_origin, example_repo.git_url) self.assertEqual(repo_name, os.path.join('/src', example_repo.oss_repo_name)) repo_origin, repo_name = build_specified_commit.detect_main_repo( test_repos.INVALID_REPO.project_name, test_repos.INVALID_REPO.new_commit) self.assertIsNone(repo_origin) self.assertIsNone(repo_name)
def test_detect_main_repo_from_commit(self): """Test the detect main repo function from build specific commit module.""" repo_origin, repo_name = build_specified_commit.detect_main_repo( 'curl', commit='bc5d22c3dede2f04870c37aec9a50474c4b888ad') self.assertEqual(repo_origin, 'https://github.com/curl/curl.git') self.assertEqual(repo_name, 'curl') repo_origin, repo_name = build_specified_commit.detect_main_repo( 'usrsctp', commit='4886aaa49fb90e479226fcfc3241d74208908232') self.assertEqual(repo_origin, 'https://github.com/weinrank/usrsctp') self.assertEqual(repo_name, 'usrsctp') repo_origin, repo_name = build_specified_commit.detect_main_repo( 'ndpi', commit='c4d476cc583a2ef1e9814134efa4fbf484564ed7') self.assertEqual(repo_origin, 'https://github.com/ntop/nDPI.git') self.assertEqual(repo_name, 'ndpi') repo_origin, repo_name = build_specified_commit.detect_main_repo( 'notproj', commit='aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa') self.assertIsNone(repo_origin) self.assertIsNone(repo_name)
def prepare_for_fuzzer_build(self): """Builds the project builder image for an OSS-Fuzz project outside of GitHub actions. Returns the repo_manager. Does not checkout source code since external projects are expected to bring their own source code to CIFuzz.""" logging.info('Building OSS-Fuzz project.') # detect_main_repo builds the image as a side effect. _, image_repo_path = (build_specified_commit.detect_main_repo( self.config.project_name, repo_name=self.config.project_repo_name)) if not image_repo_path: logging.error('Could not detect repo from project %s.', self.config.project_name) return BuildPreparationResult(False, None, None) manager = repo_manager.RepoManager(self.config.project_src_path) return BuildPreparationResult(True, image_repo_path, manager)
def build_image_and_checkout_src(self): """Builds the project builder image for a non-OSS-Fuzz project. Sets the repo manager. Does not checkout source code since external projects are expected to bring their own source code to CIFuzz. Returns True on success.""" logging.info('Building OSS-Fuzz project.') # detect_main_repo builds the image as a side effect. _, self.image_repo_path = (build_specified_commit.detect_main_repo( self.project_name, repo_name=self.project_repo_name)) if not self.image_repo_path: logging.error('Could not detect repo from project %s.', self.project_name) return False # Checkout project's repo in the shared volume. self.repo_manager = repo_manager.RepoManager(self.host_repo_path) return True
def build_fuzzers(args): """Builds all of the fuzzers for a specific OSS-Fuzz project. Returns: True on success False on failure. """ # TODO: Fix return value bubble to actually handle errors. with tempfile.TemporaryDirectory() as tmp_dir: inferred_url, repo_name = build_specified_commit.detect_main_repo( args.project_name, repo_name=args.repo_name) build_repo_manager = repo_manager.RepoManager(inferred_url, tmp_dir, repo_name=repo_name) build_data = build_specified_commit.BuildData() build_data.project_name = args.project_name build_data.sanitizer = 'address' build_data.engine = 'libfuzzer' build_data.architecture = 'x86_64' return build_specified_commit.build_fuzzers_from_commit( args.commit_sha, build_repo_manager, build_data) == 0
def build_fuzzers(project_name, project_repo_name, workspace, pr_ref=None, commit_sha=None, sanitizer='address'): """Builds all of the fuzzers for a specific OSS-Fuzz project. Args: project_name: The name of the OSS-Fuzz project being built. project_repo_name: The name of the projects repo. workspace: The location in a shared volume to store a git repo and build artifacts. pr_ref: The pull request reference to be built. commit_sha: The commit sha for the project to be built at. sanitizer: The sanitizer the fuzzers should be built with. Returns: True if build succeeded or False on failure. """ # Validate inputs. assert pr_ref or commit_sha if not os.path.exists(workspace): logging.error('Invalid workspace: %s.', workspace) return False logging.info("Using %s sanitizer.", sanitizer) out_dir = os.path.join(workspace, 'out') os.makedirs(out_dir, exist_ok=True) # Build Fuzzers using docker run. inferred_url, project_builder_repo_path = ( build_specified_commit.detect_main_repo(project_name, repo_name=project_repo_name)) if not inferred_url or not project_builder_repo_path: logging.error('Could not detect repo from project %s.', project_name) return False project_repo_name = os.path.basename(project_builder_repo_path) src_in_project_builder = os.path.dirname(project_builder_repo_path) manual_src_path = os.getenv('MANUAL_SRC_PATH') if manual_src_path: if not os.path.exists(manual_src_path): logging.error( 'MANUAL_SRC_PATH: %s does not exist. ' 'Are you mounting it correctly?', manual_src_path) return False # This is the path taken outside of GitHub actions. git_workspace = os.path.dirname(manual_src_path) else: git_workspace = os.path.join(workspace, 'storage') os.makedirs(git_workspace, exist_ok=True) # Checkout projects repo in the shared volume. build_repo_manager = repo_manager.RepoManager(inferred_url, git_workspace, repo_name=project_repo_name) if not manual_src_path: checkout_specified_commit(build_repo_manager, pr_ref, commit_sha) command = [ '--cap-add', 'SYS_PTRACE', '-e', 'FUZZING_ENGINE=' + DEFAULT_ENGINE, '-e', 'SANITIZER=' + sanitizer, '-e', 'ARCHITECTURE=' + DEFAULT_ARCHITECTURE, '-e', 'FUZZING_LANGUAGE=c++', # FIXME: Add proper support. ] container = utils.get_container_name() if container: command += ['-e', 'OUT=' + out_dir, '--volumes-from', container] bash_command = 'rm -rf {0} && cp -r {1} {2} && compile'.format( os.path.join(src_in_project_builder, project_repo_name, '*'), os.path.join(git_workspace, project_repo_name), src_in_project_builder) else: command += [ '-e', 'OUT=' + '/out', '-v', '%s:%s' % (os.path.join(git_workspace, project_repo_name), os.path.join(src_in_project_builder, project_repo_name)), '-v', '%s:%s' % (out_dir, '/out') ] bash_command = 'compile' command.extend([ 'gcr.io/oss-fuzz/' + project_name, '/bin/bash', '-c', ]) command.append(bash_command) if helper.docker_run(command): logging.error('Building fuzzers failed.') return False remove_unaffected_fuzzers(project_name, out_dir, build_repo_manager.get_git_diff(), project_builder_repo_path) return True
def build_fuzzers(project_name, project_repo_name, workspace, pr_ref=None, commit_sha=None): """Builds all of the fuzzers for a specific OSS-Fuzz project. Args: project_name: The name of the OSS-Fuzz project being built. project_repo_name: The name of the projects repo. workspace: The location in a shared volume to store a git repo and build artifacts. pr_ref: The pull request reference to be built. commit_sha: The commit sha for the project to be built at. Returns: True if build succeeded or False on failure. """ # Validate inputs. assert pr_ref or commit_sha if not os.path.exists(workspace): logging.error('Invalid workspace: %s.', workspace) return False git_workspace = os.path.join(workspace, 'storage') os.makedirs(git_workspace, exist_ok=True) out_dir = os.path.join(workspace, 'out') os.makedirs(out_dir, exist_ok=True) # Detect repo information. inferred_url, oss_fuzz_repo_path = build_specified_commit.detect_main_repo( project_name, repo_name=project_repo_name) if not inferred_url or not oss_fuzz_repo_path: logging.error('Could not detect repo from project %s.', project_name) return False src_in_docker = os.path.dirname(oss_fuzz_repo_path) oss_fuzz_repo_name = os.path.basename(oss_fuzz_repo_path) # Checkout projects repo in the shared volume. build_repo_manager = repo_manager.RepoManager(inferred_url, git_workspace, repo_name=oss_fuzz_repo_name) try: if pr_ref: build_repo_manager.checkout_pr(pr_ref) else: build_repo_manager.checkout_commit(commit_sha) except RuntimeError: logging.error('Can not check out requested state.') return False except ValueError: logging.error('Invalid commit SHA requested %s.', commit_sha) return False # Build Fuzzers using docker run. command = [ '--cap-add', 'SYS_PTRACE', '-e', 'FUZZING_ENGINE=' + DEFAULT_ENGINE, '-e', 'SANITIZER=' + DEFAULT_SANITIZER, '-e', 'ARCHITECTURE=' + DEFAULT_ARCHITECTURE ] container = utils.get_container_name() if container: command += ['-e', 'OUT=' + out_dir, '--volumes-from', container] bash_command = 'rm -rf {0} && cp -r {1} {2} && compile'.format( os.path.join(src_in_docker, oss_fuzz_repo_name, '*'), os.path.join(git_workspace, oss_fuzz_repo_name), src_in_docker) else: command += [ '-e', 'OUT=' + '/out', '-v', '%s:%s' % (os.path.join(git_workspace, oss_fuzz_repo_name), os.path.join(src_in_docker, oss_fuzz_repo_name)), '-v', '%s:%s' % (out_dir, '/out') ] bash_command = 'compile' command.extend([ 'gcr.io/oss-fuzz/' + project_name, '/bin/bash', '-c', ]) command.append(bash_command) if helper.docker_run(command): logging.error('Building fuzzers failed.') return False remove_unaffected_fuzzers(project_name, out_dir, build_repo_manager.get_git_diff(), oss_fuzz_repo_path) return True
def build_fuzzers(project_name, project_repo_name, commit_sha, git_workspace, out_dir): """Builds all of the fuzzers for a specific OSS-Fuzz project. Args: project_name: The name of the OSS-Fuzz project being built. project_repo_name: The name of the projects repo. commit_sha: The commit SHA to be checked out and fuzzed. git_workspace: The location in the shared volume to store git repos. out_dir: The location in the shared volume to store output artifacts. Returns: True if build succeeded or False on failure. """ if not os.path.exists(git_workspace): logging.error('Invalid git workspace: %s.', format(git_workspace)) return False if not os.path.exists(out_dir): logging.error('Invalid out directory %s.', format(out_dir)) return False inferred_url, oss_fuzz_repo_path = build_specified_commit.detect_main_repo( project_name, repo_name=project_repo_name) if not inferred_url or not oss_fuzz_repo_path: logging.error('Could not detect repo from project %s.', project_name) return False src_in_docker = os.path.dirname(oss_fuzz_repo_path) oss_fuzz_repo_name = os.path.basename(oss_fuzz_repo_path) # Checkout projects repo in the shared volume. build_repo_manager = repo_manager.RepoManager(inferred_url, git_workspace, repo_name=oss_fuzz_repo_name) try: build_repo_manager.checkout_commit(commit_sha) except repo_manager.RepoManagerError: logging.error('Specified commit does not exist.') # NOTE: Remove return statement for testing. return False command = [ '--cap-add', 'SYS_PTRACE', '-e', 'FUZZING_ENGINE=libfuzzer', '-e', 'SANITIZER=address', '-e', 'ARCHITECTURE=x86_64' ] container = utils.get_container_name() if container: command += ['-e', 'OUT=' + out_dir, '--volumes-from', container] bash_command = 'rm -rf {0} && cp -r {1} {2} && compile'.format( os.path.join(src_in_docker, oss_fuzz_repo_name, '*'), os.path.join(git_workspace, oss_fuzz_repo_name), src_in_docker) else: command += [ '-e', 'OUT=' + '/out', '-v', '%s:%s' % (os.path.join(git_workspace, oss_fuzz_repo_name), os.path.join(src_in_docker, oss_fuzz_repo_name)), '-v', '%s:%s' % (out_dir, '/out') ] bash_command = 'compile' command.extend([ 'gcr.io/oss-fuzz/' + project_name, '/bin/bash', '-c', ]) command.append(bash_command) if helper.docker_run(command): logging.error('Building fuzzers failed.') return False return True
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])
def bisect(old_commit, new_commit, test_case_path, fuzz_target, build_data): # pylint: disable=too-many-locals """From a commit range, this function caluclates which introduced a specific error from a fuzz test_case_path. Args: 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. """ 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.') host_src_dir = build_specified_commit.copy_src_from_docker( build_data.project_name, tmp_dir) bisect_repo_manager = repo_manager.RepoManager( repo_url, host_src_dir, repo_name=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 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 commit_list[new_idx]