class FuchsiaQemuLibFuzzerRunner(new_process.ProcessRunner, LibFuzzerCommon): """libFuzzer runner (when Fuchsia is the target platform).""" FUCHSIA_BUILD_REL_PATH = os.path.join('build', 'out', 'default') SSH_RETRIES = 3 SSH_WAIT = 3 FUZZER_TEST_DATA_REL_PATH = os.path.join('test_data', 'fuzzing') def _setup_fuzzer_and_device(self): """ Build a Fuzzer object based on the QEMU values. Call this only after setup_qemu_values()""" fuchsia_pkey_path = environment.get_value('FUCHSIA_PKEY_PATH') fuchsia_portnum = environment.get_value('FUCHSIA_PORTNUM') fuchsia_resources_dir = environment.get_value('FUCHSIA_RESOURCES_DIR') if (not fuchsia_pkey_path or not fuchsia_portnum or not fuchsia_resources_dir): raise fuchsia.errors.FuchsiaConfigError( ('FUCHSIA_PKEY_PATH, FUCHSIA_PORTNUM, or FUCHSIA_RESOURCES_DIR was ' 'not set')) fuchsia_resources_dir_plus_build = os.path.join(fuchsia_resources_dir, self.FUCHSIA_BUILD_REL_PATH) self.host = Host.from_dir(fuchsia_resources_dir_plus_build) self.device = Device(self.host, 'localhost', fuchsia_portnum) self.device.set_ssh_option('StrictHostKeyChecking no') self.device.set_ssh_option('UserKnownHostsFile=/dev/null') self.device.set_ssh_identity(fuchsia_pkey_path) # Fuchsia fuzzer names have the format {package_name}/{binary_name}. package, target = environment.get_value('FUZZ_TARGET').split('/') test_data_dir = os.path.join(fuchsia_resources_dir_plus_build, self.FUZZER_TEST_DATA_REL_PATH, package, target) self.fuzzer = Fuzzer( self.device, package, target, output=test_data_dir, foreground=True) def __init__(self, executable_path, default_args=None): qemu_path, qemu_args = setup_qemu_values(initial_setup=False) qemu_process = setup_qemu_instance(qemu_path, qemu_args) self._setup_fuzzer_and_device() self.qemu_instance = run_qemu_instance(qemu_process) super(FuchsiaQemuLibFuzzerRunner, self).__init__( executable_path=executable_path, default_args=default_args) def __del__(self): self.qemu_instance.kill() def get_command(self, additional_args=None): # TODO(flowerhack): Update this to dynamically pick a result from "fuzz # list" and then run that fuzzer. return self.ssh_command('ls') def process_logs_and_crash(self, artifact_prefix): """Fetch symbolized logs and crashes.""" if not artifact_prefix: return # Clusterfuzz assumes that the Libfuzzer output points to an absolute path, # where it can find the crash file. # This doesn't work in our case due to how Fuchsia is run. # So, we make a new file, change the appropriate line with a regex to point # to the true location. Apologies for the hackery. crash_location_regex = r'(.*)(Test unit written to )(data/.*)' _, processed_log_path = tempfile.mkstemp() with open(processed_log_path, 'w') as new_file: with open(self.fuzzer.logfile) as old_file: for line in old_file: line_match = re.match(crash_location_regex, line) if line_match: # We now know the name of our crash file. crash_name = line_match.group(3).replace('data/', '') # Save the crash locally. self.device.fetch( self.fuzzer.data_path(crash_name), artifact_prefix) # Then update the crash report to point to that file. crash_testcase_file_path = os.path.join(artifact_prefix, crash_name) line = re.sub(crash_location_regex, r'\1\2' + crash_testcase_file_path, line) new_file.write(line) os.remove(self.fuzzer.logfile) shutil.move(processed_log_path, self.fuzzer.logfile) def _test_ssh(self): """Test the ssh connection.""" # Test the connection. If this works, proceed. # - If we fail, restart QEMU and test the connection again. # - If that fails, throw the error; we can't seem to recover. try: self._test_qemu_ssh() except fuchsia.errors.FuchsiaConnectionError: self._restart_qemu() self._test_qemu_ssh() def _restart_qemu(self): """Restart QEMU.""" self.qemu_instance.kill() qemu_path, qemu_args = setup_qemu_values(initial_setup=False) qemu_process = setup_qemu_instance(qemu_path, qemu_args) self._setup_fuzzer_and_device() self.qemu_instance = run_qemu_instance(qemu_process) def fuzz(self, corpus_directories, fuzz_timeout, artifact_prefix=None, additional_args=None, extra_env=None): """LibFuzzerCommon.fuzz override.""" self._test_ssh() #TODO(flowerhack): Pass libfuzzer args (additional_args) here return_code = self.fuzzer.start(additional_args) self.fuzzer.monitor(return_code) self.process_logs_and_crash(artifact_prefix) with open(self.fuzzer.logfile) as logfile: symbolized_output = logfile.read() # TODO(flowerhack): Would be nice if we could figure out a way to make # the "fuzzer start" code return its own ProcessResult. For now, we simply # craft one by hand here. fuzzer_process_result = new_process.ProcessResult() fuzzer_process_result.return_code = 0 fuzzer_process_result.output = symbolized_output fuzzer_process_result.time_executed = 0 fuzzer_process_result.command = self.fuzzer.last_fuzz_cmd return fuzzer_process_result def run_single_testcase(self, testcase_path, timeout=None, additional_args=None): """Run a single testcase.""" self._test_ssh() # We need to push the testcase to the device and pass in the name. testcase_path_name = os.path.basename(os.path.normpath(testcase_path)) self.device.store(testcase_path, self.fuzzer.data_path()) # TODO(flowerhack): Pass libfuzzer args (additional_args) here return_code = self.fuzzer.start(['repro', 'data/' + testcase_path_name] + additional_args) self.fuzzer.monitor(return_code) with open(self.fuzzer.logfile) as logfile: symbolized_output = logfile.read() fuzzer_process_result = new_process.ProcessResult() fuzzer_process_result.return_code = 0 fuzzer_process_result.output = symbolized_output fuzzer_process_result.time_executed = 0 fuzzer_process_result.command = self.fuzzer.last_fuzz_cmd return fuzzer_process_result def minimize_crash(self, testcase_path, output_path, timeout, artifact_prefix=None, additional_args=None): return new_process.ProcessResult() def ssh_command(self, *args): return ['ssh'] + self.ssh_root + list(args) @retry.wrap(retries=SSH_RETRIES, delay=SSH_WAIT, function='_test_qemu_ssh') def _test_qemu_ssh(self): """Tests that a VM is up and can be successfully SSH'd into. Raises an exception if no success after MAX_SSH_RETRIES.""" ssh_test_process = new_process.ProcessRunner( 'ssh', self.device.get_ssh_cmd( ['ssh', 'localhost', 'echo running on fuchsia!'])[1:]) result = ssh_test_process.run_and_wait() if result.return_code or result.timed_out: raise fuchsia.errors.FuchsiaConnectionError( 'Failed to establish initial SSH connection: ' + str(result.return_code) + " , " + str(result.command) + " , " + str(result.output)) return result
class FuchsiaQemuLibFuzzerRunner(new_process.ProcessRunner, LibFuzzerCommon): """libFuzzer runner (when Fuchsia is the target platform).""" FUCHSIA_BUILD_REL_PATH = os.path.join('build', 'out', 'default') SSH_RETRIES = 3 SSH_WAIT = 3 FUZZER_TEST_DATA_REL_PATH = os.path.join('test_data', 'fuzzing') def _setup_device_and_fuzzer(self): """Build a Device and Fuzzer object based on QEMU's settings.""" # These environment variables are set when start_qemu is run. # We need them in order to ssh / otherwise communicate with the VM. fuchsia_pkey_path = environment.get_value('FUCHSIA_PKEY_PATH') fuchsia_portnum = environment.get_value('FUCHSIA_PORTNUM') fuchsia_resources_dir = environment.get_value('FUCHSIA_RESOURCES_DIR') if (not fuchsia_pkey_path or not fuchsia_portnum or not fuchsia_resources_dir): raise fuchsia.errors.FuchsiaConfigError(( 'FUCHSIA_PKEY_PATH, FUCHSIA_PORTNUM, or FUCHSIA_RESOURCES_DIR was ' 'not set')) # Fuzzer objects communicate with the VM via a Device object, # which we set up here. fuchsia_resources_dir_plus_build = os.path.join( fuchsia_resources_dir, self.FUCHSIA_BUILD_REL_PATH) self.host = Host.from_dir(fuchsia_resources_dir_plus_build) self.device = Device(self.host, 'localhost', fuchsia_portnum) self.device.set_ssh_option('StrictHostKeyChecking no') self.device.set_ssh_option('UserKnownHostsFile=/dev/null') self.device.set_ssh_identity(fuchsia_pkey_path) # Fuchsia fuzzer names have the format {package_name}/{binary_name}. package, target = self.executable_path.split('/') test_data_dir = os.path.join(fuchsia_resources_dir_plus_build, self.FUZZER_TEST_DATA_REL_PATH, package, target) # Finally, we set up the Fuzzer object itself, which will run our fuzzer! sanitizer = environment.get_memory_tool_name( environment.get_value('JOB_NAME')).lower() self.fuzzer = Fuzzer(self.device, package, target, output=test_data_dir, foreground=True, sanitizer=sanitizer) def __init__(self, executable_path, default_args=None): # We always assume QEMU is running on __init__, since build_manager sets # it up initially. If this isn't the case, _test_ssh will detect and # restart QEMU anyway. super(FuchsiaQemuLibFuzzerRunner, self).__init__(executable_path=executable_path, default_args=default_args) self._setup_device_and_fuzzer() def process_logs_and_crash(self, artifact_prefix): """Fetch symbolized logs and crashes.""" if not artifact_prefix: return # Clusterfuzz assumes that the Libfuzzer output points to an absolute path, # where it can find the crash file. # This doesn't work in our case due to how Fuchsia is run. # So, we make a new file, change the appropriate line with a regex to point # to the true location. Apologies for the hackery. crash_location_regex = r'(.*)(Test unit written to )(data/.*)' _, processed_log_path = tempfile.mkstemp() with open(processed_log_path, 'w') as new_file: with open(self.fuzzer.logfile) as old_file: for line in old_file: line_match = re.match(crash_location_regex, line) if line_match: # We now know the name of our crash file. crash_name = line_match.group(3).replace('data/', '') # Save the crash locally. self.device.fetch(self.fuzzer.data_path(crash_name), artifact_prefix) # Then update the crash report to point to that file. crash_testcase_file_path = os.path.join( artifact_prefix, crash_name) line = re.sub(crash_location_regex, r'\1\2' + crash_testcase_file_path, line) new_file.write(line) os.remove(self.fuzzer.logfile) shutil.move(processed_log_path, self.fuzzer.logfile) def _test_ssh(self): """Test the ssh connection.""" # Test the connection. If this works, proceed. # - If we fail, restart QEMU and test the connection again. # - If that fails, throw the error; we can't seem to recover. try: self._test_qemu_ssh() except fuchsia.errors.FuchsiaConnectionError: self._restart_qemu() self._test_qemu_ssh() @retry.wrap(retries=SSH_RETRIES, delay=SSH_WAIT, function='_test_qemu_ssh') def _test_qemu_ssh(self): """Tests that a VM is up and can be successfully SSH'd into. Raises an exception if no success after MAX_SSH_RETRIES.""" ssh_test_process = new_process.ProcessRunner( 'ssh', self.device.get_ssh_cmd( ['ssh', 'localhost', 'echo running on fuchsia!'])[1:]) result = ssh_test_process.run_and_wait() if result.return_code or result.timed_out: raise fuchsia.errors.FuchsiaConnectionError( 'Failed to establish initial SSH connection: ' + str(result.return_code) + " , " + str(result.command) + " , " + str(result.output)) return result def _restart_qemu(self): """Restart QEMU.""" logs.log_warn('Connection to fuzzing VM lost. Restarting.') stop_qemu() start_qemu() self._setup_device_and_fuzzer() def _corpus_target_subdir(self, relpath): """ Returns the absolute path of the corpus subdirectory on the target, given "relpath", the name of the specific corpus. """ return os.path.join(self._corpus_directories_target(), relpath) def _corpus_directories_libfuzzer(self, corpus_directories): """ Returns the corpus directory paths expected by libfuzzer itself. """ corpus_directories_libfuzzer = [] for corpus_dir in corpus_directories: corpus_directories_libfuzzer.append( os.path.join('data', 'corpus', os.path.basename(corpus_dir))) return corpus_directories_libfuzzer def _new_corpus_dir_host(self, corpus_directories): """ Returns the path of the 'new' corpus directory on the host. """ return corpus_directories[0] def _new_corpus_dir_target(self, corpus_directories): """ Returns the path of the 'new' corpus directory on the target. """ new_corpus_dir_host = self._new_corpus_dir_host(corpus_directories) return self.fuzzer.data_path( os.path.join('corpus', os.path.basename(new_corpus_dir_host))) def _corpus_directories_target(self): """ Returns the path of the root corpus directory on the target. """ return self.fuzzer.data_path('corpus') def _push_corpora_from_host_to_target(self, corpus_directories): # Push corpus directories to the device. self._clear_all_target_corpora() logs.log('Push corpora from host to target.') for corpus_dir in corpus_directories: # Appending '/*' indicates we want all the *files* in the corpus_dir's self.fuzzer.device.store( corpus_dir + '/*', self._corpus_target_subdir(os.path.basename(corpus_dir))) def _pull_new_corpus_from_target_to_host(self, corpus_directories): # Appending '/*' indicates we want all the *files* in the target's # directory, rather than the directory itself. logs.log('Fuzzer ran; pull down corpus') files_in_new_corpus_dir_target = self._new_corpus_dir_target( corpus_directories) + "/*" self.fuzzer.device.fetch(files_in_new_corpus_dir_target, self._new_corpus_dir_host(corpus_directories)) def _clear_all_target_corpora(self): """ Clears out all the corpora on the target. """ logs.log('Clearing corpora on target') self.fuzzer.device.ssh( ['rm', '-rf', self._corpus_directories_target()]) def fuzz(self, corpus_directories, fuzz_timeout, artifact_prefix=None, additional_args=None, extra_env=None): """LibFuzzerCommon.fuzz override.""" additional_args = copy.copy(additional_args) if additional_args is None: additional_args = [] self._test_ssh() self._push_corpora_from_host_to_target(corpus_directories) max_total_time = self.get_max_total_time(fuzz_timeout) if any(arg.startswith(constants.FORK_FLAG) for arg in additional_args): max_total_time -= self.LIBFUZZER_FORK_MODE_CLEAN_EXIT_TIME assert max_total_time > 0 additional_args.extend([ '%s%d' % (constants.MAX_TOTAL_TIME_FLAG, max_total_time), constants.PRINT_FINAL_STATS_ARGUMENT, ]) # Run the fuzzer. # TODO: actually we want new_corpus_relative_dir_target for *each* corpus return_code = self.fuzzer.start( self._corpus_directories_libfuzzer(corpus_directories) + additional_args) self.fuzzer.monitor(return_code) self.process_logs_and_crash(artifact_prefix) with open(self.fuzzer.logfile) as logfile: symbolized_output = logfile.read() self._pull_new_corpus_from_target_to_host(corpus_directories) self._clear_all_target_corpora() # TODO(flowerhack): Would be nice if we could figure out a way to make # the "fuzzer start" code return its own ProcessResult. For now, we simply # craft one by hand here. fuzzer_process_result = new_process.ProcessResult() fuzzer_process_result.return_code = 0 fuzzer_process_result.output = symbolized_output fuzzer_process_result.time_executed = 0 fuzzer_process_result.command = self.fuzzer.last_fuzz_cmd return fuzzer_process_result def merge(self, corpus_directories, merge_timeout, artifact_prefix=None, tmp_dir=None, additional_args=None): # TODO(flowerhack): Integrate some notion of a merge timeout. self._push_corpora_from_host_to_target(corpus_directories) # Run merge. _, _ = self.fuzzer.merge( self._corpus_directories_libfuzzer(corpus_directories) + additional_args) self._pull_new_corpus_from_target_to_host(corpus_directories) self._clear_all_target_corpora() merge_result = new_process.ProcessResult() merge_result.return_code = 0 merge_result.timed_out = False merge_result.output = '' merge_result.time_executed = 0 merge_result.command = '' return merge_result def run_single_testcase(self, testcase_path, timeout=None, additional_args=None): """Run a single testcase.""" self._test_ssh() # We need to push the testcase to the device and pass in the name. testcase_path_name = os.path.basename(os.path.normpath(testcase_path)) self.device.store(testcase_path, self.fuzzer.data_path()) return_code = self.fuzzer.start( ['repro', 'data/' + testcase_path_name] + additional_args) self.fuzzer.monitor(return_code) with open(self.fuzzer.logfile) as logfile: symbolized_output = logfile.read() fuzzer_process_result = new_process.ProcessResult() fuzzer_process_result.return_code = 0 fuzzer_process_result.output = symbolized_output fuzzer_process_result.time_executed = 0 fuzzer_process_result.command = self.fuzzer.last_fuzz_cmd return fuzzer_process_result def minimize_crash(self, testcase_path, output_path, timeout, artifact_prefix=None, additional_args=None): return new_process.ProcessResult() def ssh_command(self, *args): return ['ssh'] + self.ssh_root + list(args)