def run_fuzzers(config): # pylint: disable=too-many-locals """Runs fuzzers for a specific OSS-Fuzz project. Args: fuzz_seconds: The total time allotted for fuzzing. workspace: The location in a shared volume to store a git repo and build artifacts. project_name: The name of the relevant OSS-Fuzz project. sanitizer: The sanitizer the fuzzers should be run with. Returns: (True if run was successful, True if bug was found). """ # Validate inputs. logging.info('Using %s sanitizer.', config.sanitizer) out_dir = os.path.join(config.workspace, 'out') artifacts_dir = os.path.join(out_dir, 'artifacts') os.makedirs(artifacts_dir, exist_ok=True) if not config.fuzz_seconds or config.fuzz_seconds < 1: logging.error( 'Fuzz_seconds argument must be greater than 1, but was: %s.', config.fuzz_seconds) return False, False # Get fuzzer information. fuzzer_paths = utils.get_fuzz_targets(out_dir) if not fuzzer_paths: logging.error('No fuzzers were found in out directory: %s.', out_dir) return False, False # Run fuzzers for allotted time. total_num_fuzzers = len(fuzzer_paths) fuzzers_left_to_run = total_num_fuzzers min_seconds_per_fuzzer = config.fuzz_seconds // total_num_fuzzers for fuzzer_path in fuzzer_paths: run_seconds = max(config.fuzz_seconds // fuzzers_left_to_run, min_seconds_per_fuzzer) target = fuzz_target.FuzzTarget(fuzzer_path, run_seconds, out_dir, config.project_name, sanitizer=config.sanitizer) start_time = time.time() testcase, stacktrace = target.fuzz() config.fuzz_seconds -= (time.time() - start_time) if not testcase or not stacktrace: logging.info('Fuzzer %s, finished running.', target.target_name) else: utils.binary_print(b'Fuzzer %s, detected error:\n%s' % (target.target_name.encode(), stacktrace)) shutil.move(testcase, os.path.join(artifacts_dir, 'test_case')) stack_parser.parse_fuzzer_output(stacktrace, artifacts_dir) return True, True fuzzers_left_to_run -= 1 return True, False
def fuzz(self, use_corpus=True, extra_libfuzzer_options=None): """Starts the fuzz target run for the length of time specified by duration. Returns: FuzzResult namedtuple with stacktrace and testcase if applicable. """ logging.info('Running fuzzer: %s.', self.target_name) if extra_libfuzzer_options is None: extra_libfuzzer_options = [] env = base_runner_utils.get_env(self.config, self.workspace) # TODO(metzman): Is this needed? env['RUN_FUZZER_MODE'] = 'interactive' if use_corpus: # If corpus can be downloaded, use it for fuzzing. self._download_corpus() env['CORPUS_DIR'] = self.latest_corpus_path options = LIBFUZZER_OPTIONS.copy() + [ f'-max_total_time={self.duration}', # Make sure libFuzzer artifact files don't pollute $OUT. f'-artifact_prefix={self.workspace.artifacts}/' ] + extra_libfuzzer_options command = ['run_fuzzer', self.target_name] + options logging.info('Running command: %s', command) process = subprocess.Popen(command, env=env, stdout=subprocess.PIPE, stderr=subprocess.PIPE) try: _, stderr = process.communicate(timeout=self.duration + BUFFER_TIME) except subprocess.TimeoutExpired: logging.error('Fuzzer %s timed out, ending fuzzing.', self.target_name) return FuzzResult(None, None, self.latest_corpus_path) # Libfuzzer timeout was reached. if not process.returncode: logging.info('Fuzzer %s finished with no crashes discovered.', self.target_name) return FuzzResult(None, None, self.latest_corpus_path) # Crash was discovered. logging.info('Fuzzer %s, ended before timeout.', self.target_name) testcase = get_testcase(stderr) if not testcase: logging.error(b'No testcase found in stacktrace: %s.', stderr) return FuzzResult(None, None, self.latest_corpus_path) utils.binary_print(b'Fuzzer: %s. Detected bug:\n%s' % (self.target_name.encode(), stderr)) if self.is_crash_reportable(testcase): # We found a bug in the fuzz target and we will report it. return FuzzResult(testcase, stderr, self.latest_corpus_path) # We found a bug but we won't report it. return FuzzResult(None, None, self.latest_corpus_path)
def run_fuzz_targets(self): """Runs fuzz targets. Returns True if a bug was found.""" fuzzers_left_to_run = len(self.fuzz_target_paths) # Make a copy since we will mutate it. fuzz_seconds = self.config.fuzz_seconds min_seconds_per_fuzzer = fuzz_seconds // fuzzers_left_to_run bug_found = False for target_path in self.fuzz_target_paths: # By doing this, we can ensure that every fuzz target runs for at least # min_seconds_per_fuzzer, but that other fuzzers will have longer to run # if one ends early. run_seconds = max(fuzz_seconds // fuzzers_left_to_run, min_seconds_per_fuzzer) target = self.create_fuzz_target_obj(target_path, run_seconds) start_time = time.time() result = self.run_fuzz_target(target) # It's OK if this goes negative since we take max when determining # run_seconds. fuzz_seconds -= time.time() - start_time fuzzers_left_to_run -= 1 if not result.testcase or not result.stacktrace: logging.info('Fuzzer %s finished running without crashes.', target.target_name) continue # We found a bug in the fuzz target. utils.binary_print( b'Fuzzer: %s. Detected bug:\n%s' % (target.target_name.encode(), result.stacktrace)) # TODO(metzman): Do this with filestore. testcase_artifact_path = self.get_fuzz_target_artifact( target, 'testcase') shutil.move(result.testcase, testcase_artifact_path) bug_summary_artifact_path = self.get_fuzz_target_artifact( target, 'bug-summary.txt') stack_parser.parse_fuzzer_output(result.stacktrace, bug_summary_artifact_path) bug_found = True if self.quit_on_bug_found: logging.info('Bug found. Stopping fuzzing.') return bug_found return bug_found
def fuzz(self): """Starts the fuzz target run for the length of time specified by duration. Returns: FuzzResult namedtuple with stacktrace and testcase if applicable. """ logging.info('Fuzzer %s, started.', self.target_name) docker_container = utils.get_container_name() command = ['docker', 'run', '--rm', '--privileged'] if docker_container: command += [ '--volumes-from', docker_container, '-e', 'OUT=' + self.out_dir ] else: command += ['-v', '%s:%s' % (self.out_dir, '/out')] command += [ '-e', 'FUZZING_ENGINE=libfuzzer', '-e', 'SANITIZER=' + self.config.sanitizer, '-e', 'CIFUZZ=True', '-e', 'RUN_FUZZER_MODE=interactive', docker.BASE_RUNNER_TAG, 'bash', '-c' ] run_fuzzer_command = 'run_fuzzer {fuzz_target} {options}'.format( fuzz_target=self.target_name, options=LIBFUZZER_OPTIONS + ' -max_total_time=' + str(self.duration)) # If corpus can be downloaded use it for fuzzing. self.latest_corpus_path = self.clusterfuzz_deployment.download_corpus( self.target_name, self.out_dir) if self.latest_corpus_path: run_fuzzer_command = run_fuzzer_command + ' ' + self.latest_corpus_path command.append(run_fuzzer_command) logging.info('Running command: %s', ' '.join(command)) process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE) try: _, stderr = process.communicate(timeout=self.duration + BUFFER_TIME) except subprocess.TimeoutExpired: logging.error('Fuzzer %s timed out, ending fuzzing.', self.target_name) return FuzzResult(None, None) # Libfuzzer timeout was reached. if not process.returncode: logging.info('Fuzzer %s finished with no crashes discovered.', self.target_name) return FuzzResult(None, None) # Crash was discovered. logging.info('Fuzzer %s, ended before timeout.', self.target_name) testcase = self.get_testcase(stderr) if not testcase: logging.error(b'No testcase found in stacktrace: %s.', stderr) return FuzzResult(None, None) utils.binary_print(b'Fuzzer: %s. Detected bug:\n%s' % (self.target_name.encode(), stderr)) if self.is_crash_reportable(testcase): # We found a bug in the fuzz target and we will report it. return FuzzResult(testcase, stderr) # We found a bug but we won't report it. return FuzzResult(None, None)
def fuzz(self): """Starts the fuzz target run for the length of time specified by duration. Returns: FuzzResult namedtuple with stacktrace and testcase if applicable. """ logging.info('Running fuzzer: %s.', self.target_name) command, _ = docker.get_base_docker_run_command(self.workspace, self.config.sanitizer, self.config.language) # If corpus can be downloaded use it for fuzzing. self.latest_corpus_path = self.clusterfuzz_deployment.download_corpus( self.target_name) command += ['-e', 'CORPUS_DIR=' + self.latest_corpus_path] command += [ '-e', 'RUN_FUZZER_MODE=interactive', docker.BASE_RUNNER_TAG, 'bash', '-c' ] options = LIBFUZZER_OPTIONS.copy() + [ f'-max_total_time={self.duration}', # Make sure libFuzzer artifact files don't pollute $OUT. f'-artifact_prefix={self.workspace.artifacts}/' ] options = ' '.join(options) run_fuzzer_command = f'run_fuzzer {self.target_name} {options}' command.append(run_fuzzer_command) logging.info('Running command: %s', ' '.join(command)) process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE) try: _, stderr = process.communicate(timeout=self.duration + BUFFER_TIME) except subprocess.TimeoutExpired: logging.error('Fuzzer %s timed out, ending fuzzing.', self.target_name) return FuzzResult(None, None, self.latest_corpus_path) # Libfuzzer timeout was reached. if not process.returncode: logging.info('Fuzzer %s finished with no crashes discovered.', self.target_name) return FuzzResult(None, None, self.latest_corpus_path) # Crash was discovered. logging.info('Fuzzer %s, ended before timeout.', self.target_name) testcase = get_testcase(stderr) if not testcase: logging.error(b'No testcase found in stacktrace: %s.', stderr) return FuzzResult(None, None, self.latest_corpus_path) utils.binary_print(b'Fuzzer: %s. Detected bug:\n%s' % (self.target_name.encode(), stderr)) if self.is_crash_reportable(testcase): # We found a bug in the fuzz target and we will report it. return FuzzResult(testcase, stderr, self.latest_corpus_path) # We found a bug but we won't report it. return FuzzResult(None, None, self.latest_corpus_path)
def fuzz(self): """Starts the fuzz target run for the length of time specified by duration. Returns: FuzzResult namedtuple with stacktrace and testcase if applicable. """ logging.info('Fuzzer %s, started.', self.target_name) docker_container = utils.get_container_name() command_arguments = [] if docker_container: command_arguments += [ '--volumes-from', docker_container, '-e', 'OUT=' + self.out_dir ] else: command_arguments += ['-v', '%s:%s' % (self.out_dir, '/out')] command_arguments += [ '-e', 'FUZZING_ENGINE=libfuzzer', '-e', 'SANITIZER=' + self.config.sanitizer, '-e', 'CIFUZZ=True', '-e', 'RUN_FUZZER_MODE=interactive', docker.BASE_RUNNER_TAG, 'bash', '-c' ] run_fuzzer_command = 'run_fuzzer {fuzz_target} {options}'.format( fuzz_target=self.target_name, options=LIBFUZZER_OPTIONS + ' -max_total_time=' + str(self.duration)) # If corpus can be downloaded use it for fuzzing. self.latest_corpus_path = self.clusterfuzz_deployment.download_corpus( self.target_name, self.out_dir) if self.latest_corpus_path: run_fuzzer_command = run_fuzzer_command + ' ' + self.latest_corpus_path command_arguments.append(run_fuzzer_command) result = docker.run_container_command(command_arguments, timeout=self.duration + BUFFER_TIME) if result.timed_out: logging.info('Stopped docker container before timeout.') if not result.retcode: logging.info('Fuzzer %s finished with no crashes discovered.', self.target_name) return FuzzResult(None, None) if result.stderr is None: return FuzzResult(None, None) # Crash was discovered. logging.info('Fuzzer %s, ended before timeout.', self.target_name) # TODO(metzman): Replace this with artifact_prefix so we don't have to # parse. testcase = self.get_testcase(result.stderr) if not testcase: logging.error(b'No testcase found in stacktrace: %s.', result.stderr) return FuzzResult(None, None) utils.binary_print(b'Fuzzer: %s. Detected bug:\n%s' % (self.target_name.encode(), result.stderr)) if self.is_crash_reportable(testcase): # We found a bug in the fuzz target and we will report it. return FuzzResult(testcase, result.stderr) # We found a bug but we won't report it. return FuzzResult(None, None)
def test_binary_string(self): # pylint: disable=no-self-use """Tests that utils.binary_print can print a bianry string.""" # Should execute without raising any exceptions. with mock.patch('sys.stdout.buffer.write') as mock_write: utils.binary_print(b'hello') mock_write.assert_called_with(b'hello\n')