def remove_fuzzing_arguments(arguments): """Remove arguments used during fuzzing.""" for argument in [ constants.DICT_FLAG, # User for fuzzing only. constants.MAX_LEN_FLAG, # This may shrink the testcases. constants.RUNS_FLAG, # Make sure we don't have any '-runs' argument. constants.FORK_FLAG, # It overrides `-merge` argument. constants.COLLECT_DATA_FLOW_FLAG, # Used for fuzzing only. ]: fuzzer_utils.extract_argument(arguments, argument)
def expand_with_common_arguments(arguments): """Return list of arguments expanded by necessary common options.""" # We prefer to add common arguments (either default or custom) in run.py, but # in order to maintain compatibility with previously found testcases, we do # add necessary common arguments here as well. common_arguments = [] if not fuzzer_utils.extract_argument( arguments, constants.RSS_LIMIT_FLAG, remove=False): common_arguments.append( '%s%d' % (constants.RSS_LIMIT_FLAG, constants.DEFAULT_RSS_LIMIT_MB)) if not fuzzer_utils.extract_argument( arguments, constants.TIMEOUT_FLAG, remove=False): common_arguments.append( '%s%d' % (constants.TIMEOUT_FLAG, constants.DEFAULT_TIMEOUT_LIMIT)) return arguments + common_arguments
def from_target_path(cls, target_path): """Instantiates and returns an AFLConfig object. The object is configured based on |target_path|.""" config = cls() config.parse_options(target_path) config.dict_path = fuzzer_utils.extract_argument( config.additional_afl_arguments, constants.DICT_FLAG, remove=False) config.use_default_dict(target_path) return config
def remove_fuzzing_arguments(arguments): """Remove arguments used during fuzzing.""" # Remove un-needed dictionary argument. fuzzer_utils.extract_argument(arguments, constants.DICT_FLAG) # 'max_len' option may shrink the testcase if its size greater than 'max_len'. fuzzer_utils.extract_argument(arguments, constants.MAX_LEN_FLAG) # Remove custom '-runs' argument. fuzzer_utils.extract_argument(arguments, constants.RUNS_FLAG) # Remove `-fork' argument since it overrides '-merge' argument. fuzzer_utils.extract_argument(arguments, constants.FORK_FLAG)
def add_recommended_dictionary(arguments, fuzzer_name, fuzzer_path): """Add recommended dictionary from GCS to existing .dict file or create a new one and update the arguments as needed. This function modifies |arguments| list in some cases.""" recommended_dictionary_path = os.path.join( fuzzer_utils.get_temp_dir(), dictionary_manager.RECOMMENDED_DICTIONARY_FILENAME) dict_manager = dictionary_manager.DictionaryManager(fuzzer_name) try: # Bail out if cannot download recommended dictionary from GCS. if not dict_manager.download_recommended_dictionary_from_gcs( recommended_dictionary_path): return False except Exception as ex: logs.log_error('Exception downloading recommended dictionary:\n%s.' % str(ex)) return False # Bail out if the downloaded dictionary is empty. if not os.path.getsize(recommended_dictionary_path): return False # Check if there is an existing dictionary file in arguments. original_dictionary_path = fuzzer_utils.extract_argument( arguments, constants.DICT_FLAG) merged_dictionary_path = ( original_dictionary_path or dictionary_manager.get_default_dictionary_path(fuzzer_path)) merged_dictionary_path += MERGED_DICT_SUFFIX dictionary_manager.merge_dictionary_files(original_dictionary_path, recommended_dictionary_path, merged_dictionary_path) arguments.append(constants.DICT_FLAG + merged_dictionary_path) return True
def main(argv): """Run libFuzzer as specified by argv.""" atexit.register(fuzzer_utils.cleanup) # Initialize variables. arguments = argv[1:] testcase_file_path = arguments.pop(0) target_name = arguments.pop(0) fuzzer_name = data_types.fuzz_target_project_qualified_name( utils.current_project(), target_name) # Initialize log handler. logs.configure( 'run_fuzzer', { 'fuzzer': fuzzer_name, 'engine': 'libFuzzer', 'job_name': environment.get_value('JOB_NAME') }) profiler.start_if_needed('libfuzzer_launcher') # Make sure that the fuzzer binary exists. build_directory = environment.get_value('BUILD_DIR') fuzzer_path = engine_common.find_fuzzer_path(build_directory, target_name) if not fuzzer_path: # This is an expected case when doing regression testing with old builds # that do not have that fuzz target. It can also happen when a host sends a # message to an untrusted worker that just restarted and lost information on # build directory. logs.log_warn('Could not find fuzz target %s.' % target_name) return # Install signal handler. signal.signal(signal.SIGTERM, engine_common.signal_term_handler) # Set up temp dir. engine_common.recreate_directory(fuzzer_utils.get_temp_dir()) # Setup minijail if needed. use_minijail = environment.get_value('USE_MINIJAIL') runner = libfuzzer.get_runner(fuzzer_path, temp_dir=fuzzer_utils.get_temp_dir()) if use_minijail: minijail_chroot = runner.chroot else: minijail_chroot = None # Get corpus directory. corpus_directory = environment.get_value('FUZZ_CORPUS_DIR') # Add common arguments which are necessary to be used for every run. arguments = expand_with_common_arguments(arguments) # Add sanitizer options to environment that were specified in the .options # file and options that this script requires. set_sanitizer_options(fuzzer_path) # Minimize test argument. minimize_to = fuzzer_utils.extract_argument(arguments, MINIMIZE_TO_ARGUMENT) minimize_timeout = fuzzer_utils.extract_argument( arguments, MINIMIZE_TIMEOUT_ARGUMENT) if minimize_to and minimize_timeout: minimize_testcase(runner, testcase_file_path, minimize_to, int(minimize_timeout), arguments, use_minijail) return # Cleanse argument. cleanse_to = fuzzer_utils.extract_argument(arguments, CLEANSE_TO_ARGUMENT) cleanse_timeout = fuzzer_utils.extract_argument(arguments, CLEANSE_TIMEOUT_ARGUMENT) if cleanse_to and cleanse_timeout: cleanse_testcase(runner, testcase_file_path, cleanse_to, int(cleanse_timeout), arguments, use_minijail) return # If we don't have a corpus, then that means this is not a fuzzing run. if not corpus_directory: load_testcase_if_exists(runner, testcase_file_path, fuzzer_name, use_minijail, arguments) return # We don't have a crash testcase, fuzz. # Check dict argument to make sure that it's valid. dict_argument = fuzzer_utils.extract_argument(arguments, constants.DICT_FLAG, remove=False) if dict_argument and not os.path.exists(dict_argument): logs.log_error('Invalid dict %s for %s.' % (dict_argument, fuzzer_name)) fuzzer_utils.extract_argument(arguments, constants.DICT_FLAG) # If there's no dict argument, check for %target_binary_name%.dict file. if (not fuzzer_utils.extract_argument( arguments, constants.DICT_FLAG, remove=False)): default_dict_path = dictionary_manager.get_default_dictionary_path( fuzzer_path) if os.path.exists(default_dict_path): arguments.append(constants.DICT_FLAG + default_dict_path) fuzzing_strategies = [] # Select a generator to use for existing testcase mutations. generator = _select_generator() is_mutations_run = generator != Generator.NONE # Timeout for fuzzer run. fuzz_timeout = get_fuzz_timeout(is_mutations_run) # Set up scratch directory for writing new units. new_testcases_directory = create_corpus_directory('new') # Get list of corpus directories. corpus_directories = get_corpus_directories(corpus_directory, new_testcases_directory, fuzzer_path, fuzzing_strategies, minijail_chroot) # Bind corpus directories in minijail. if use_minijail: artifact_prefix = constants.ARTIFACT_PREFIX_FLAG + '/' else: artifact_prefix = '%s%s/' % (constants.ARTIFACT_PREFIX_FLAG, os.path.abspath( os.path.dirname(testcase_file_path))) # Generate new testcase mutations using radamsa, etc. if is_mutations_run: new_testcase_mutations_directory = generate_new_testcase_mutations( corpus_directory, fuzzer_name, generator, fuzzing_strategies) corpus_directories.append(new_testcase_mutations_directory) if use_minijail: bind_corpus_dirs(minijail_chroot, [new_testcase_mutations_directory]) max_len_argument = fuzzer_utils.extract_argument(arguments, constants.MAX_LEN_FLAG, remove=False) if not max_len_argument and do_random_max_length(): max_length = random.SystemRandom().randint(1, MAX_VALUE_FOR_MAX_LENGTH) arguments.append('%s%d' % (constants.MAX_LEN_FLAG, max_length)) fuzzing_strategies.append(strategy.RANDOM_MAX_LENGTH_STRATEGY) if do_recommended_dictionary(): if add_recommended_dictionary(arguments, fuzzer_name, fuzzer_path): fuzzing_strategies.append(strategy.RECOMMENDED_DICTIONARY_STRATEGY) if do_value_profile(): arguments.append(constants.VALUE_PROFILE_ARGUMENT) fuzzing_strategies.append(strategy.VALUE_PROFILE_STRATEGY) if do_fork(): max_fuzz_threads = environment.get_value('MAX_FUZZ_THREADS', 1) num_fuzz_processes = max( 1, multiprocessing.cpu_count() // max_fuzz_threads) arguments.append('%s%d' % (constants.FORK_FLAG, num_fuzz_processes)) fuzzing_strategies.append('%s_%d' % (strategy.FORK_STRATEGY, num_fuzz_processes)) extra_env = {} if do_mutator_plugin(): if use_mutator_plugin(target_name, extra_env, minijail_chroot): fuzzing_strategies.append(strategy.MUTATOR_PLUGIN_STRATEGY) # Execute the fuzzer binary with original arguments. fuzz_result = runner.fuzz(corpus_directories, fuzz_timeout=fuzz_timeout, additional_args=arguments + [artifact_prefix], extra_env=extra_env) if (not use_minijail and fuzz_result.return_code == constants.LIBFUZZER_ERROR_EXITCODE): # Minijail returns 1 if the exit code is nonzero. # Otherwise: we can assume that a return code of 1 means that libFuzzer # itself ran into an error. logs.log_error(ENGINE_ERROR_MESSAGE, engine_output=fuzz_result.output) log_lines = fuzz_result.output.splitlines() # Output can be large, so save some memory by removing reference to the # original output which is no longer needed. fuzz_result.output = None # Check if we crashed, and get the crash testcase path. crash_testcase_file_path = None for line in log_lines: match = re.match(CRASH_TESTCASE_REGEX, line) if match: crash_testcase_file_path = match.group(1) break if crash_testcase_file_path: # Write the new testcase. if use_minijail: # Convert chroot relative path to host path. Remove the leading '/' before # joining. crash_testcase_file_path = os.path.join( minijail_chroot.directory, crash_testcase_file_path[1:]) # Copy crash testcase contents into the main testcase path. shutil.move(crash_testcase_file_path, testcase_file_path) # Print the command output. log_header_format = ('Command: %s\n' 'Bot: %s\n' 'Time ran: %f\n') bot_name = environment.get_value('BOT_NAME', '') command = fuzz_result.command if use_minijail: # Remove minijail prefix. command = engine_common.strip_minijail_command(command, fuzzer_path) print(log_header_format % (engine_common.get_command_quoted(command), bot_name, fuzz_result.time_executed)) # Parse stats information based on libFuzzer output. parsed_stats = parse_log_stats(log_lines) # Extend parsed stats by additional performance features. parsed_stats.update( stats.parse_performance_features(log_lines, fuzzing_strategies, arguments)) # Set some initial stat overrides. timeout_limit = fuzzer_utils.extract_argument(arguments, constants.TIMEOUT_FLAG, remove=False) expected_duration = runner.get_max_total_time(fuzz_timeout) actual_duration = int(fuzz_result.time_executed) fuzzing_time_percent = 100 * actual_duration / float(expected_duration) stat_overrides = { 'timeout_limit': int(timeout_limit), 'expected_duration': expected_duration, 'actual_duration': actual_duration, 'fuzzing_time_percent': fuzzing_time_percent, } # Remove fuzzing arguments before merge and dictionary analysis step. remove_fuzzing_arguments(arguments) # Make a decision on whether merge step is needed at all. If there are no # new units added by libFuzzer run, then no need to do merge at all. new_units_added = shell.get_directory_file_count(new_testcases_directory) merge_error = None if new_units_added: # Merge the new units with the initial corpus. if corpus_directory not in corpus_directories: corpus_directories.append(corpus_directory) # If this times out, it's possible that we will miss some units. However, if # we're taking >10 minutes to load/merge the corpus something is going very # wrong and we probably don't want to make things worse by adding units # anyway. merge_tmp_dir = None if not use_minijail: merge_tmp_dir = os.path.join(fuzzer_utils.get_temp_dir(), 'merge_workdir') engine_common.recreate_directory(merge_tmp_dir) old_corpus_len = shell.get_directory_file_count(corpus_directory) merge_directory = create_merge_directory() corpus_directories.insert(0, merge_directory) if use_minijail: bind_corpus_dirs(minijail_chroot, [merge_directory]) merge_result = runner.merge( corpus_directories, merge_timeout=engine_common.get_merge_timeout( DEFAULT_MERGE_TIMEOUT), tmp_dir=merge_tmp_dir, additional_args=arguments) move_mergeable_units(merge_directory, corpus_directory) new_corpus_len = shell.get_directory_file_count(corpus_directory) new_units_added = 0 merge_error = None if merge_result.timed_out: merge_error = 'Merging new testcases timed out:' elif merge_result.return_code != 0: merge_error = 'Merging new testcases failed:' else: new_units_added = new_corpus_len - old_corpus_len stat_overrides['new_units_added'] = new_units_added if merge_result.output: stat_overrides.update( stats.parse_stats_from_merge_log( merge_result.output.splitlines())) else: stat_overrides['new_units_added'] = 0 logs.log('Skipped corpus merge since no new units added by fuzzing.') # Get corpus size after merge. This removes the duplicate units that were # created during this fuzzing session. stat_overrides['corpus_size'] = shell.get_directory_file_count( corpus_directory) # Delete all corpus directories except for the main one. These were temporary # directories to store new testcase mutations and have already been merged to # main corpus directory. if corpus_directory in corpus_directories: corpus_directories.remove(corpus_directory) for directory in corpus_directories: shutil.rmtree(directory, ignore_errors=True) if use_minijail: unbind_corpus_dirs(minijail_chroot, corpus_directories) # Apply overridden stats to the parsed stats prior to dumping. parsed_stats.update(stat_overrides) # Dump stats data for further uploading to BigQuery. engine_common.dump_big_query_data(parsed_stats, testcase_file_path, LIBFUZZER_PREFIX, fuzzer_name, command) # Add custom crash state based on fuzzer name (if needed). add_custom_crash_state_if_needed(fuzzer_name, log_lines, parsed_stats) for line in log_lines: print(line) # Add fuzzing strategies used. engine_common.print_fuzzing_strategies(fuzzing_strategies) # Add merge error (if any). if merge_error: print(data_types.CRASH_STACKTRACE_END_MARKER) print(merge_error) print( 'Command:', get_printable_command(merge_result.command, fuzzer_path, use_minijail)) print(merge_result.output) analyze_and_update_recommended_dictionary(runner, fuzzer_name, log_lines, corpus_directory, arguments) # Close minijail chroot. if use_minijail: minijail_chroot.close() # Record the stats to make them easily searchable in stackdriver. if new_units_added: logs.log('New units added to corpus: %d.' % new_units_added, stats=parsed_stats) else: logs.log('No new units found.', stats=parsed_stats)
def prepare(self, corpus_dir, target_path, _): """Prepare for a fuzzing session, by generating options. Returns a FuzzOptions object. Args: corpus_dir: The main corpus directory. target_path: Path to the target. build_dir: Path to the build directory. Returns: A FuzzOptions object. """ arguments = fuzzer.get_arguments(target_path) strategy_pool = strategy_selection.generate_weighted_strategy_pool( strategy_list=strategy.LIBFUZZER_STRATEGY_LIST, use_generator=True, engine_name=self.name) strategy_info = launcher.pick_strategies(strategy_pool, target_path, corpus_dir, arguments) arguments.extend(strategy_info.arguments) # Check for seed corpus and add it into corpus directory. engine_common.unpack_seed_corpus_if_needed(target_path, corpus_dir) # Pick a few testcases from our corpus to use as the initial corpus. subset_size = engine_common.random_choice( engine_common.CORPUS_SUBSET_NUM_TESTCASES) if (not strategy_info.use_dataflow_tracing and strategy_pool.do_strategy(strategy.CORPUS_SUBSET_STRATEGY) and shell.get_directory_file_count(corpus_dir) > subset_size): # Copy |subset_size| testcases into 'subset' directory. corpus_subset_dir = self._create_temp_corpus_dir('subset') launcher.copy_from_corpus(corpus_subset_dir, corpus_dir, subset_size) strategy_info.fuzzing_strategies.append( strategy.CORPUS_SUBSET_STRATEGY.name + '_' + str(subset_size)) strategy_info.additional_corpus_dirs.append(corpus_subset_dir) else: strategy_info.additional_corpus_dirs.append(corpus_dir) # Check dict argument to make sure that it's valid. dict_argument = fuzzer_utils.extract_argument(arguments, constants.DICT_FLAG, remove=False) if dict_argument and not os.path.exists(dict_argument): logs.log_error('Invalid dict %s for %s.' % (dict_argument, target_path)) fuzzer_utils.extract_argument(arguments, constants.DICT_FLAG) # If there's no dict argument, check for %target_binary_name%.dict file. if (not fuzzer_utils.extract_argument( arguments, constants.DICT_FLAG, remove=False)): default_dict_path = dictionary_manager.get_default_dictionary_path( target_path) if os.path.exists(default_dict_path): arguments.append(constants.DICT_FLAG + default_dict_path) return LibFuzzerOptions(corpus_dir, arguments, strategy_info.fuzzing_strategies, strategy_info.additional_corpus_dirs, strategy_info.extra_env, strategy_info.use_dataflow_tracing, strategy_info.is_mutations_run)
def fuzz(self, target_path, options, reproducers_dir, max_time): """Run a fuzz session. Args: target_path: Path to the target. options: The FuzzOptions object returned by prepare(). reproducers_dir: The directory to put reproducers in when crashes are found. max_time: Maximum allowed time for the fuzzing to run. Returns: A Result object. """ profiler.start_if_needed('libfuzzer_fuzz') runner = libfuzzer.get_runner(target_path) launcher.set_sanitizer_options(target_path) artifact_prefix = self._artifact_prefix( os.path.abspath(reproducers_dir)) # Directory to place new units. new_corpus_dir = self._create_temp_corpus_dir('new') corpus_directories = [new_corpus_dir] + options.fuzz_corpus_dirs fuzz_timeout = launcher.get_fuzz_timeout(options.is_mutations_run, total_timeout=max_time) fuzz_result = runner.fuzz(corpus_directories, fuzz_timeout=fuzz_timeout, additional_args=options.arguments + [artifact_prefix], extra_env=options.extra_env) log_lines = fuzz_result.output.splitlines() # Output can be large, so save some memory by removing reference to the # original output which is no longer needed. fuzz_result.output = None # Check if we crashed, and get the crash testcase path. crash_testcase_file_path = None for line in log_lines: match = re.match(launcher.CRASH_TESTCASE_REGEX, line) if match: crash_testcase_file_path = match.group(1) break # Parse stats information based on libFuzzer output. parsed_stats = launcher.parse_log_stats(log_lines) # Extend parsed stats by additional performance features. parsed_stats.update( stats.parse_performance_features(log_lines, options.strategies, options.arguments, include_strategies=False)) # Set some initial stat overrides. timeout_limit = fuzzer_utils.extract_argument(options.arguments, constants.TIMEOUT_FLAG, remove=False) expected_duration = runner.get_max_total_time(fuzz_timeout) actual_duration = int(fuzz_result.time_executed) fuzzing_time_percent = 100 * actual_duration / float(expected_duration) parsed_stats.update({ 'timeout_limit': int(timeout_limit), 'expected_duration': expected_duration, 'actual_duration': actual_duration, 'fuzzing_time_percent': fuzzing_time_percent, }) # Remove fuzzing arguments before merge and dictionary analysis step. arguments = options.arguments[:] launcher.remove_fuzzing_arguments(arguments) self._merge_new_units(target_path, options.corpus_dir, new_corpus_dir, options.fuzz_corpus_dirs, arguments, parsed_stats) # Add custom crash state based on fuzzer name (if needed). project_qualified_fuzzer_name = ( data_types.fuzz_target_project_qualified_name( utils.current_project(), os.path.basename(target_path))) launcher.add_custom_crash_state_if_needed( project_qualified_fuzzer_name, log_lines, parsed_stats) fuzz_logs = '\n'.join(log_lines) crashes = [] if crash_testcase_file_path: # Write the new testcase. # Copy crash testcase contents into the main testcase path. crashes.append( engine.Crash(crash_testcase_file_path, fuzz_logs, arguments, actual_duration)) launcher.analyze_and_update_recommended_dictionary( runner, project_qualified_fuzzer_name, log_lines, options.corpus_dir, arguments) return engine.Result(fuzz_logs, fuzz_result.command, crashes, parsed_stats, fuzz_result.time_executed)
def parse_performance_features(log_lines, strategies, arguments): """Extract stats for performance analysis.""" # Initialize stats with default values. stats = { 'bad_instrumentation': 0, 'corpus_crash_count': 0, 'corpus_size': 0, 'crash_count': 0, 'dict_used': 0, 'edge_coverage': 0, 'edges_total': 0, 'feature_coverage': 0, 'initial_edge_coverage': 0, 'initial_feature_coverage': 0, 'leak_count': 0, 'log_lines_unwanted': 0, 'log_lines_from_engine': 0, 'log_lines_ignored': 0, 'max_len': 0, 'manual_dict_size': 0, 'merge_edge_coverage': 0, 'merge_new_files': 0, 'merge_new_features': 0, 'oom_count': 0, 'recommended_dict_size': 0, 'slow_unit_count': 0, 'slow_units_count': 0, 'startup_crash_count': 1, 'strategy_corpus_mutations_radamsa': 0, 'strategy_corpus_mutations_ml_rnn': 0, 'strategy_corpus_subset': 0, 'strategy_fork': 0, 'strategy_mutator_plugin': 0, 'strategy_random_max_len': 0, 'strategy_recommended_dict': 0, 'strategy_value_profile': 0, 'timeout_count': 0, } # Process fuzzing strategies used. stats.update(parse_fuzzing_strategies(log_lines, strategies)) # Corpus rss is only applicable when using the full corpus and not # in the corpus subset strategy run. if not stats['strategy_corpus_subset']: stats['corpus_rss_mb'] = 0 (stats['log_lines_unwanted'], stats['log_lines_from_engine'], stats['log_lines_ignored']) = calculate_log_lines(log_lines) # Extract '-max_len' value from arguments, if possible. stats['max_len'] = int( fuzzer_utils.extract_argument( arguments, constants.MAX_LEN_FLAG, remove=False) or stats['max_len']) # Extract sizes of manual and recommended dictionary used for fuzzing. dictionary_path = fuzzer_utils.extract_argument( arguments, constants.DICT_FLAG, remove=False) stats['manual_dict_size'], stats['recommended_dict_size'] = ( dictionary_manager.get_stats_for_dictionary_file(dictionary_path)) # Different crashes and other flags extracted via regexp match. has_corpus = False for line in log_lines: if LIBFUZZER_BAD_INSTRUMENTATION_REGEX.match(line): stats['bad_instrumentation'] = 1 continue if LIBFUZZER_CRASH_TESTCASE_REGEX.match(line): stats['crash_count'] = 1 continue if LIBFUZZER_LOG_DICTIONARY_REGEX.match(line): stats['dict_used'] = 1 continue if LEAK_TESTCASE_REGEX.match(line): stats['leak_count'] = 1 continue if (LIBFUZZER_OOM_TESTCASE_REGEX.match(line) or stack_analyzer.OUT_OF_MEMORY_REGEX.match(line)): stats['oom_count'] = 1 continue if LIBFUZZER_SLOW_UNIT_TESTCASE_REGEX.match(line): # Use |slow_unit_count| to track if this run had any slow units at all. # and use |slow_units_count| to track the actual number of slow units in # this run (used by performance analyzer). stats['slow_unit_count'] = 1 stats['slow_units_count'] += 1 continue match = LIBFUZZER_LOG_SEED_CORPUS_INFO_REGEX.match(line) if match: has_corpus = True if not stats['strategy_corpus_subset']: stats['corpus_rss_mb'] = int(match.group(2)) match = LIBFUZZER_MODULES_LOADED_REGEX.match(line) if match: stats['startup_crash_count'] = 0 stats['edges_total'] = int(match.group(2)) match = LIBFUZZER_LOG_START_INITED_REGEX.match(line) if match: stats['initial_edge_coverage'] = int(match.group(1)) stats['initial_feature_coverage'] = int(match.group(2)) continue # This regexp will match multiple lines and will be overwriting the stats. # This is done on purpose, as the last line in the log may have different # format, e.g. 'DONE' without a crash and 'NEW' or 'pulse' with a crash. match = LIBFUZZER_LOG_COVERAGE_REGEX.match(line) if match: stats['edge_coverage'] = int(match.group(1)) stats['feature_coverage'] = int(match.group(2)) continue if (LIBFUZZER_TIMEOUT_TESTCASE_REGEX.match(line) or stack_analyzer.LIBFUZZER_TIMEOUT_REGEX.match(line)): stats['timeout_count'] = 1 continue if not stats['max_len']: # Get "max_len" value from the log, if it has not been found in arguments. match = LIBFUZZER_LOG_MAX_LEN_REGEX.match(line) if match: stats['max_len'] = int(match.group(1)) continue if has_corpus and not stats['log_lines_from_engine']: stats['corpus_crash_count'] = 1 return stats
def main(argv): """Run libFuzzer as specified by argv.""" atexit.register(fuzzer_utils.cleanup) # Initialize variables. arguments = argv[1:] testcase_file_path = arguments.pop(0) target_name = environment.get_value('FUZZ_TARGET') if arguments and arguments[0] == target_name: # Pop legacy fuzz target argument. arguments.pop(0) fuzzer_name = data_types.fuzz_target_project_qualified_name( utils.current_project(), target_name) # Initialize log handler. logs.configure( 'run_fuzzer', { 'fuzzer': fuzzer_name, 'engine': 'libFuzzer', 'job_name': environment.get_value('JOB_NAME') }) profiler.start_if_needed('libfuzzer_launcher') # Make sure that the fuzzer binary exists. build_directory = environment.get_value('BUILD_DIR') fuzzer_path = engine_common.find_fuzzer_path(build_directory, target_name) if not fuzzer_path: return # Install signal handler. signal.signal(signal.SIGTERM, engine_common.signal_term_handler) # Set up temp dir. engine_common.recreate_directory(fuzzer_utils.get_temp_dir()) # Setup minijail if needed. use_minijail = environment.get_value('USE_MINIJAIL') runner = libfuzzer.get_runner( fuzzer_path, temp_dir=fuzzer_utils.get_temp_dir()) if use_minijail: minijail_chroot = runner.chroot else: minijail_chroot = None # Get corpus directory. corpus_directory = environment.get_value('FUZZ_CORPUS_DIR') # Add common arguments which are necessary to be used for every run. arguments = expand_with_common_arguments(arguments) # Add sanitizer options to environment that were specified in the .options # file and options that this script requires. set_sanitizer_options(fuzzer_path) # If we don't have a corpus, then that means this is not a fuzzing run. if not corpus_directory: load_testcase_if_exists(runner, testcase_file_path, fuzzer_name, use_minijail, arguments) return # We don't have a crash testcase, fuzz. # Check dict argument to make sure that it's valid. dict_argument = fuzzer_utils.extract_argument( arguments, constants.DICT_FLAG, remove=False) if dict_argument and not os.path.exists(dict_argument): logs.log_error('Invalid dict %s for %s.' % (dict_argument, fuzzer_name)) fuzzer_utils.extract_argument(arguments, constants.DICT_FLAG) # If there's no dict argument, check for %target_binary_name%.dict file. if (not fuzzer_utils.extract_argument( arguments, constants.DICT_FLAG, remove=False)): default_dict_path = dictionary_manager.get_default_dictionary_path( fuzzer_path) if os.path.exists(default_dict_path): arguments.append(constants.DICT_FLAG + default_dict_path) # Set up scratch directory for writing new units. new_testcases_directory = create_corpus_directory('new') # Strategy pool is the list of strategies that we attempt to enable, whereas # fuzzing strategies is the list of strategies that are enabled. (e.g. if # mutator is selected in the pool, but not available for a given target, it # would not be added to fuzzing strategies.) strategy_pool = strategy_selection.generate_weighted_strategy_pool( strategy_list=strategy.LIBFUZZER_STRATEGY_LIST, use_generator=True, engine_name='libFuzzer') strategy_info = pick_strategies( strategy_pool, fuzzer_path, corpus_directory, arguments, minijail_chroot=minijail_chroot) arguments.extend(strategy_info.arguments) # Timeout for fuzzer run. fuzz_timeout = get_fuzz_timeout(strategy_info.is_mutations_run) # Get list of corpus directories. # TODO(flowerhack): Implement this to handle corpus sync'ing. if environment.platform() == 'FUCHSIA': corpus_directories = [] else: corpus_directories = get_corpus_directories( corpus_directory, new_testcases_directory, fuzzer_path, strategy_info.fuzzing_strategies, strategy_pool, minijail_chroot=minijail_chroot, allow_corpus_subset=not strategy_info.use_dataflow_tracing) corpus_directories.extend(strategy_info.additional_corpus_dirs) artifact_prefix = os.path.abspath(os.path.dirname(testcase_file_path)) # Execute the fuzzer binary with original arguments. fuzz_result = runner.fuzz( corpus_directories, fuzz_timeout=fuzz_timeout, artifact_prefix=artifact_prefix, additional_args=arguments, extra_env=strategy_info.extra_env) if (not use_minijail and fuzz_result.return_code == constants.LIBFUZZER_ERROR_EXITCODE): # Minijail returns 1 if the exit code is nonzero. # Otherwise: we can assume that a return code of 1 means that libFuzzer # itself ran into an error. logs.log_error(ENGINE_ERROR_MESSAGE, engine_output=fuzz_result.output) log_lines = fuzz_result.output.splitlines() # Output can be large, so save some memory by removing reference to the # original output which is no longer needed. fuzz_result.output = None # Check if we crashed, and get the crash testcase path. crash_testcase_file_path = runner.get_testcase_path(log_lines) if crash_testcase_file_path: # Copy crash testcase contents into the main testcase path. shutil.move(crash_testcase_file_path, testcase_file_path) # Print the command output. bot_name = environment.get_value('BOT_NAME', '') command = fuzz_result.command if use_minijail: # Remove minijail prefix. command = engine_common.strip_minijail_command(command, fuzzer_path) print(engine_common.get_log_header(command, bot_name, fuzz_result.time_executed)) # Parse stats information based on libFuzzer output. parsed_stats = parse_log_stats(log_lines) # Extend parsed stats by additional performance features. parsed_stats.update( stats.parse_performance_features( log_lines, strategy_info.fuzzing_strategies, arguments)) # Set some initial stat overrides. timeout_limit = fuzzer_utils.extract_argument( arguments, constants.TIMEOUT_FLAG, remove=False) expected_duration = runner.get_max_total_time(fuzz_timeout) actual_duration = int(fuzz_result.time_executed) fuzzing_time_percent = 100 * actual_duration / float(expected_duration) stat_overrides = { 'timeout_limit': int(timeout_limit), 'expected_duration': expected_duration, 'actual_duration': actual_duration, 'fuzzing_time_percent': fuzzing_time_percent, } # Remove fuzzing arguments before merge and dictionary analysis step. remove_fuzzing_arguments(arguments) # Make a decision on whether merge step is needed at all. If there are no # new units added by libFuzzer run, then no need to do merge at all. new_units_added = shell.get_directory_file_count(new_testcases_directory) merge_error = None if new_units_added: # Merge the new units with the initial corpus. if corpus_directory not in corpus_directories: corpus_directories.append(corpus_directory) # If this times out, it's possible that we will miss some units. However, if # we're taking >10 minutes to load/merge the corpus something is going very # wrong and we probably don't want to make things worse by adding units # anyway. merge_tmp_dir = None if not use_minijail: merge_tmp_dir = os.path.join(fuzzer_utils.get_temp_dir(), 'merge_workdir') engine_common.recreate_directory(merge_tmp_dir) old_corpus_len = shell.get_directory_file_count(corpus_directory) merge_directory = create_merge_directory() corpus_directories.insert(0, merge_directory) if use_minijail: bind_corpus_dirs(minijail_chroot, [merge_directory]) merge_result = runner.merge( corpus_directories, merge_timeout=engine_common.get_merge_timeout(DEFAULT_MERGE_TIMEOUT), tmp_dir=merge_tmp_dir, additional_args=arguments) move_mergeable_units(merge_directory, corpus_directory) new_corpus_len = shell.get_directory_file_count(corpus_directory) new_units_added = 0 merge_error = None if merge_result.timed_out: merge_error = 'Merging new testcases timed out:' elif merge_result.return_code != 0: merge_error = 'Merging new testcases failed:' else: new_units_added = new_corpus_len - old_corpus_len stat_overrides['new_units_added'] = new_units_added if merge_result.output: stat_overrides.update( stats.parse_stats_from_merge_log(merge_result.output.splitlines())) else: stat_overrides['new_units_added'] = 0 logs.log('Skipped corpus merge since no new units added by fuzzing.') # Get corpus size after merge. This removes the duplicate units that were # created during this fuzzing session. # TODO(flowerhack): Remove this workaround once we can handle corpus sync. if environment.platform() != 'FUCHSIA': stat_overrides['corpus_size'] = shell.get_directory_file_count( corpus_directory) # Delete all corpus directories except for the main one. These were temporary # directories to store new testcase mutations and have already been merged to # main corpus directory. if corpus_directory in corpus_directories: corpus_directories.remove(corpus_directory) for directory in corpus_directories: shutil.rmtree(directory, ignore_errors=True) if use_minijail: unbind_corpus_dirs(minijail_chroot, corpus_directories) # Apply overridden stats to the parsed stats prior to dumping. parsed_stats.update(stat_overrides) # Dump stats data for further uploading to BigQuery. engine_common.dump_big_query_data(parsed_stats, testcase_file_path, command) # Add custom crash state based on fuzzer name (if needed). add_custom_crash_state_if_needed(fuzzer_name, log_lines, parsed_stats) for line in log_lines: print(line) # Add fuzzing strategies used. print(engine_common.format_fuzzing_strategies( strategy_info.fuzzing_strategies)) # Add merge error (if any). if merge_error: print(data_types.CRASH_STACKTRACE_END_MARKER) print(merge_error) print('Command:', get_printable_command(merge_result.command, fuzzer_path, use_minijail)) print(merge_result.output) analyze_and_update_recommended_dictionary(runner, fuzzer_name, log_lines, corpus_directory, arguments) # Close minijail chroot. if use_minijail: minijail_chroot.close() # Record the stats to make them easily searchable in stackdriver. if new_units_added: logs.log( 'New units added to corpus: %d.' % new_units_added, stats=parsed_stats) else: logs.log('No new units found.', stats=parsed_stats)
def pick_strategies(strategy_pool, fuzzer_path, corpus_directory, existing_arguments, minijail_chroot=None): """Pick strategies.""" build_directory = environment.get_value('BUILD_DIR') target_name = os.path.basename(fuzzer_path) project_qualified_fuzzer_name = data_types.fuzz_target_project_qualified_name( utils.current_project(), target_name) fuzzing_strategies = [] arguments = [] additional_corpus_dirs = [] # Select a generator to attempt to use for existing testcase mutations. candidate_generator = engine_common.select_generator(strategy_pool, fuzzer_path) is_mutations_run = candidate_generator != engine_common.Generator.NONE # Depends on the presense of DFSan instrumented build. dataflow_build_dir = environment.get_value('DATAFLOW_BUILD_DIR') use_dataflow_tracing = ( dataflow_build_dir and strategy_pool.do_strategy(strategy.DATAFLOW_TRACING_STRATEGY)) if use_dataflow_tracing: dataflow_binary_path = os.path.join( dataflow_build_dir, os.path.relpath(fuzzer_path, build_directory)) if os.path.exists(dataflow_binary_path): arguments.append( '%s%s' % (constants.COLLECT_DATA_FLOW_FLAG, dataflow_binary_path)) fuzzing_strategies.append(strategy.DATAFLOW_TRACING_STRATEGY.name) else: logs.log_error( 'Fuzz target is not found in dataflow build, skiping strategy.') use_dataflow_tracing = False # Generate new testcase mutations using radamsa, etc. if is_mutations_run: new_testcase_mutations_directory = create_corpus_directory('mutations') generator_used = engine_common.generate_new_testcase_mutations( corpus_directory, new_testcase_mutations_directory, project_qualified_fuzzer_name, candidate_generator) # Add the used generator strategy to our fuzzing strategies list. if generator_used: if candidate_generator == engine_common.Generator.RADAMSA: fuzzing_strategies.append( strategy.CORPUS_MUTATION_RADAMSA_STRATEGY.name) elif candidate_generator == engine_common.Generator.ML_RNN: fuzzing_strategies.append(strategy.CORPUS_MUTATION_ML_RNN_STRATEGY.name) additional_corpus_dirs.append(new_testcase_mutations_directory) if minijail_chroot: bind_corpus_dirs(minijail_chroot, [new_testcase_mutations_directory]) if strategy_pool.do_strategy(strategy.RANDOM_MAX_LENGTH_STRATEGY): max_len_argument = fuzzer_utils.extract_argument( existing_arguments, constants.MAX_LEN_FLAG, remove=False) if not max_len_argument: max_length = random.SystemRandom().randint(1, MAX_VALUE_FOR_MAX_LENGTH) arguments.append('%s%d' % (constants.MAX_LEN_FLAG, max_length)) fuzzing_strategies.append(strategy.RANDOM_MAX_LENGTH_STRATEGY.name) if (strategy_pool.do_strategy(strategy.RECOMMENDED_DICTIONARY_STRATEGY) and add_recommended_dictionary(arguments, project_qualified_fuzzer_name, fuzzer_path)): fuzzing_strategies.append(strategy.RECOMMENDED_DICTIONARY_STRATEGY.name) if strategy_pool.do_strategy(strategy.VALUE_PROFILE_STRATEGY): arguments.append(constants.VALUE_PROFILE_ARGUMENT) fuzzing_strategies.append(strategy.VALUE_PROFILE_STRATEGY.name) # DataFlow Tracing requires fork mode, always use it with DFT strategy. if use_dataflow_tracing or strategy_pool.do_strategy(strategy.FORK_STRATEGY): max_fuzz_threads = environment.get_value('MAX_FUZZ_THREADS', 1) num_fuzz_processes = max(1, multiprocessing.cpu_count() // max_fuzz_threads) arguments.append('%s%d' % (constants.FORK_FLAG, num_fuzz_processes)) fuzzing_strategies.append( '%s_%d' % (strategy.FORK_STRATEGY.name, num_fuzz_processes)) extra_env = {} if (strategy_pool.do_strategy(strategy.MUTATOR_PLUGIN_STRATEGY) and use_mutator_plugin(target_name, extra_env, minijail_chroot)): fuzzing_strategies.append(strategy.MUTATOR_PLUGIN_STRATEGY.name) return StrategyInfo(fuzzing_strategies, arguments, additional_corpus_dirs, extra_env, use_dataflow_tracing, is_mutations_run)
def parse_performance_features(log_lines, strategies, arguments): """Extract stats for performance analysis.""" # Initialize stats with default values. stats = { 'bad_instrumentation': 0, 'corpus_crash_count': 0, 'corpus_size': 0, 'crash_count': 0, 'dict_used': 0, 'edge_coverage': 0, 'edges_total': 0, 'feature_coverage': 0, 'initial_edge_coverage': 0, 'initial_feature_coverage': 0, 'leak_count': 0, 'log_lines_unwanted': 0, 'log_lines_from_engine': 0, 'log_lines_ignored': 0, 'max_len': 0, 'manual_dict_size': 0, 'merge_edge_coverage': 0, 'new_edges': 0, 'new_features': 0, 'oom_count': 0, 'recommended_dict_size': 0, 'slow_unit_count': 0, 'slow_units_count': 0, 'startup_crash_count': 1, 'timeout_count': 0, } # Extract strategy selection method. stats['strategy_selection_method'] = environment.get_value( 'STRATEGY_SELECTION_METHOD', default_value='default') # Initialize all strategy stats as disabled by default. for strategy_type in strategy.strategy_list: stats[strategy_column_name(strategy_type.name)] = 0 # Process fuzzing strategies used. stats.update(parse_fuzzing_strategies(log_lines, strategies)) (stats['log_lines_unwanted'], stats['log_lines_from_engine'], stats['log_lines_ignored']) = calculate_log_lines(log_lines) # Extract '-max_len' value from arguments, if possible. stats['max_len'] = int( fuzzer_utils.extract_argument( arguments, constants.MAX_LEN_FLAG, remove=False) or stats['max_len']) # Extract sizes of manual and recommended dictionary used for fuzzing. dictionary_path = fuzzer_utils.extract_argument( arguments, constants.DICT_FLAG, remove=False) stats['manual_dict_size'], stats['recommended_dict_size'] = ( dictionary_manager.get_stats_for_dictionary_file(dictionary_path)) # Different crashes and other flags extracted via regexp match. has_corpus = False libfuzzer_inited = False for line in log_lines: if LIBFUZZER_BAD_INSTRUMENTATION_REGEX.match(line): stats['bad_instrumentation'] = 1 continue if LIBFUZZER_CRASH_TESTCASE_REGEX.match(line): stats['crash_count'] = 1 continue if LIBFUZZER_LOG_DICTIONARY_REGEX.match(line): stats['dict_used'] = 1 continue if LEAK_TESTCASE_REGEX.match(line): stats['leak_count'] = 1 continue if (LIBFUZZER_OOM_TESTCASE_REGEX.match(line) or stack_analyzer.OUT_OF_MEMORY_REGEX.match(line)): stats['oom_count'] = 1 continue if LIBFUZZER_SLOW_UNIT_TESTCASE_REGEX.match(line): # Use |slow_unit_count| to track if this run had any slow units at all. # and use |slow_units_count| to track the actual number of slow units in # this run (used by performance analyzer). stats['slow_unit_count'] = 1 stats['slow_units_count'] += 1 continue match = LIBFUZZER_LOG_SEED_CORPUS_INFO_REGEX.match(line) if match: has_corpus = True match = LIBFUZZER_MODULES_LOADED_REGEX.match(line) if match: stats['startup_crash_count'] = 0 stats['edges_total'] = int(match.group(2)) match = LIBFUZZER_LOG_START_INITED_REGEX.match(line) if match: stats['initial_edge_coverage'] = stats['edge_coverage'] = int( match.group(1)) stats['initial_feature_coverage'] = stats['feature_coverage'] = int( match.group(2)) libfuzzer_inited = True continue # This regexp will match multiple lines and will be overwriting the stats. # This is done on purpose, as the last line in the log may have different # format, e.g. 'DONE' without a crash and 'NEW' or 'pulse' with a crash. # Also, ignore values before INITED i.e. while seed corpus is being read. match = LIBFUZZER_LOG_COVERAGE_REGEX.match(line) if match and libfuzzer_inited: stats['edge_coverage'] = int(match.group(1)) stats['feature_coverage'] = int(match.group(2)) continue if (LIBFUZZER_TIMEOUT_TESTCASE_REGEX.match(line) or stack_analyzer.LIBFUZZER_TIMEOUT_REGEX.match(line)): stats['timeout_count'] = 1 continue if not stats['max_len']: # Get "max_len" value from the log, if it has not been found in arguments. match = LIBFUZZER_LOG_MAX_LEN_REGEX.match(line) if match: stats['max_len'] = int(match.group(1)) continue if has_corpus and not stats['log_lines_from_engine']: stats['corpus_crash_count'] = 1 # new_cov_* is a reliable metric when corpus subset strategy is not used. if not stats['strategy_corpus_subset']: assert stats['edge_coverage'] >= stats['initial_edge_coverage'] stats['new_edges'] = ( stats['edge_coverage'] - stats['initial_edge_coverage']) assert stats['feature_coverage'] >= stats['initial_feature_coverage'] stats['new_features'] = ( stats['feature_coverage'] - stats['initial_feature_coverage']) return stats
def parse_performance_features(log_lines, strategies, arguments): """Extract stats for performance analysis.""" # TODO(ochang): Remove include_strategies once refactor is complete. # Initialize stats with default values. stats = { 'bad_instrumentation': 0, 'corpus_crash_count': 0, 'corpus_size': 0, 'crash_count': 0, 'dict_used': 0, 'edge_coverage': 0, 'edges_total': 0, 'feature_coverage': 0, 'initial_edge_coverage': 0, 'initial_feature_coverage': 0, 'leak_count': 0, 'log_lines_unwanted': 0, 'log_lines_from_engine': 0, 'log_lines_ignored': 0, 'max_len': 0, 'manual_dict_size': 0, 'merge_edge_coverage': 0, 'new_edges': 0, 'new_features': 0, 'oom_count': 0, 'recommended_dict_size': 0, 'slow_unit_count': 0, 'slow_units_count': 0, 'startup_crash_count': 1, 'timeout_count': 0, } # Extract strategy selection method. # TODO(ochang): Move to more general place? stats['strategy_selection_method'] = environment.get_value( 'STRATEGY_SELECTION_METHOD', default_value='default') # Initialize all strategy stats as disabled by default. for strategy_type in strategy.LIBFUZZER_STRATEGY_LIST: stats[strategy_column_name(strategy_type.name)] = 0 # Process fuzzing strategies used. stats.update(parse_fuzzing_strategies(log_lines, strategies)) (stats['log_lines_unwanted'], stats['log_lines_from_engine'], stats['log_lines_ignored']) = calculate_log_lines(log_lines) if stats['log_lines_from_engine'] > 0: stats['startup_crash_count'] = 0 # Extract '-max_len' value from arguments, if possible. stats['max_len'] = int( fuzzer_utils.extract_argument( arguments, constants.MAX_LEN_FLAG, remove=False) or stats['max_len']) # Extract sizes of manual and recommended dictionary used for fuzzing. dictionary_path = fuzzer_utils.extract_argument(arguments, constants.DICT_FLAG, remove=False) stats['manual_dict_size'], stats['recommended_dict_size'] = ( dictionary_manager.get_stats_for_dictionary_file(dictionary_path)) # Different crashes and other flags extracted via regexp match. has_corpus = False for line in log_lines: if LIBFUZZER_BAD_INSTRUMENTATION_REGEX.match(line): stats['bad_instrumentation'] = 1 continue if LIBFUZZER_CRASH_TESTCASE_REGEX.match(line): stats['crash_count'] = 1 continue if LIBFUZZER_LOG_DICTIONARY_REGEX.match(line): stats['dict_used'] = 1 continue if LEAK_TESTCASE_REGEX.match(line): stats['leak_count'] = 1 continue if (LIBFUZZER_OOM_TESTCASE_REGEX.match(line) or stack_analyzer.OUT_OF_MEMORY_REGEX.match(line)): stats['oom_count'] = 1 continue if LIBFUZZER_SLOW_UNIT_TESTCASE_REGEX.match(line): # Use |slow_unit_count| to track if this run had any slow units at all. # and use |slow_units_count| to track the actual number of slow units in # this run (used by performance analyzer). stats['slow_unit_count'] = 1 stats['slow_units_count'] += 1 continue match = LIBFUZZER_LOG_SEED_CORPUS_INFO_REGEX.match(line) if match: has_corpus = True match = LIBFUZZER_MODULES_LOADED_REGEX.match(line) if match: stats['startup_crash_count'] = 0 stats['edges_total'] = int(match.group(2)) if (LIBFUZZER_TIMEOUT_TESTCASE_REGEX.match(line) or stack_analyzer.LIBFUZZER_TIMEOUT_REGEX.match(line)): stats['timeout_count'] = 1 continue if not stats['max_len']: # Get "max_len" value from the log, if it has not been found in arguments. match = LIBFUZZER_LOG_MAX_LEN_REGEX.match(line) if match: stats['max_len'] = int(match.group(1)) continue if has_corpus and not stats['log_lines_from_engine']: stats['corpus_crash_count'] = 1 return stats
try: # Bail out if cannot download recommended dictionary from GCS. if not dict_manager.download_recommended_dictionary_from_gcs( recommended_dictionary_path): return False except Exception, ex: logs.log_error('Exception downloading recommended dictionary:\n%s.' % str(ex)) return False # Bail out if the downloaded dictionary is empty. if not os.path.getsize(recommended_dictionary_path): return False # Check if there is an existing dictionary file in arguments. original_dictionary_path = fuzzer_utils.extract_argument( arguments, constants.DICT_FLAG) merged_dictionary_path = ( original_dictionary_path or dictionary_manager.get_default_dictionary_path(fuzzer_path)) merged_dictionary_path += MERGED_DICT_SUFFIX dictionary_manager.merge_dictionary_files(original_dictionary_path, recommended_dictionary_path, merged_dictionary_path) arguments.append(constants.DICT_FLAG + merged_dictionary_path) return True def get_dictionary_analysis_timeout(): """Get timeout for dictionary analysis.""" return engine_common.get_overridable_timeout(
def fuzz(self, target_path, options, reproducers_dir, max_time): """Run a fuzz session. Args: target_path: Path to the target. options: The FuzzOptions object returned by prepare(). reproducers_dir: The directory to put reproducers in when crashes are found. max_time: Maximum allowed time for the fuzzing to run. Returns: A FuzzResult object. """ profiler.start_if_needed('libfuzzer_fuzz') runner = libfuzzer.get_runner(target_path) libfuzzer.set_sanitizer_options(target_path) # Directory to place new units. new_corpus_dir = self._create_temp_corpus_dir('new') corpus_directories = [new_corpus_dir] + options.fuzz_corpus_dirs fuzz_timeout = libfuzzer.get_fuzz_timeout(options.is_mutations_run, total_timeout=max_time) fuzz_result = runner.fuzz(corpus_directories, fuzz_timeout=fuzz_timeout, additional_args=options.arguments, artifact_prefix=reproducers_dir, extra_env=options.extra_env) if (not environment.get_value('USE_MINIJAIL') and fuzz_result.return_code == constants.LIBFUZZER_ERROR_EXITCODE): # Minijail returns 1 if the exit code is nonzero. # Otherwise: we can assume that a return code of 1 means that libFuzzer # itself ran into an error. logs.log_error(ENGINE_ERROR_MESSAGE, engine_output=fuzz_result.output) log_lines = utils.decode_to_unicode(fuzz_result.output).splitlines() # Output can be large, so save some memory by removing reference to the # original output which is no longer needed. fuzz_result.output = None # Check if we crashed, and get the crash testcase path. crash_testcase_file_path = runner.get_testcase_path(log_lines) # Parse stats information based on libFuzzer output. parsed_stats = libfuzzer.parse_log_stats(log_lines) # Extend parsed stats by additional performance features. parsed_stats.update( stats.parse_performance_features(log_lines, options.strategies, options.arguments, include_strategies=False)) # Set some initial stat overrides. timeout_limit = fuzzer_utils.extract_argument(options.arguments, constants.TIMEOUT_FLAG, remove=False) expected_duration = runner.get_max_total_time(fuzz_timeout) actual_duration = int(fuzz_result.time_executed) fuzzing_time_percent = 100 * actual_duration / float(expected_duration) parsed_stats.update({ 'timeout_limit': int(timeout_limit), 'expected_duration': expected_duration, 'actual_duration': actual_duration, 'fuzzing_time_percent': fuzzing_time_percent, }) # Remove fuzzing arguments before merge and dictionary analysis step. arguments = options.arguments[:] libfuzzer.remove_fuzzing_arguments(arguments) self._merge_new_units(target_path, options.corpus_dir, new_corpus_dir, options.fuzz_corpus_dirs, arguments, parsed_stats) fuzz_logs = '\n'.join(log_lines) crashes = [] if crash_testcase_file_path: # Use higher timeout for reproduction. reproduce_arguments = arguments[:] libfuzzer.fix_timeout_argument_for_reproduction( reproduce_arguments) # Write the new testcase. # Copy crash testcase contents into the main testcase path. crashes.append( engine.Crash(crash_testcase_file_path, fuzz_logs, reproduce_arguments, actual_duration)) project_qualified_fuzzer_name = ( data_types.fuzz_target_project_qualified_name( utils.current_project(), os.path.basename(target_path))) libfuzzer.analyze_and_update_recommended_dictionary( runner, project_qualified_fuzzer_name, log_lines, options.corpus_dir, arguments) return engine.FuzzResult(fuzz_logs, fuzz_result.command, crashes, parsed_stats, fuzz_result.time_executed)
def fuzz(self, target_path, options, reproducers_dir, max_time): """Run a fuzz session. Args: target_path: Path to the target. options: The FuzzOptions object returned by prepare(). reproducers_dir: The directory to put reproducers in when crashes are found. max_time: Maximum allowed time for the fuzzing to run. Returns: A FuzzResult object. """ profiler.start_if_needed("libfuzzer_fuzz") runner = libfuzzer.get_runner(target_path) libfuzzer.set_sanitizer_options(target_path) # Directory to place new units. new_corpus_dir = self._create_temp_corpus_dir("new") corpus_directories = [new_corpus_dir] + options.fuzz_corpus_dirs fuzz_timeout = libfuzzer.get_fuzz_timeout(options.is_mutations_run, total_timeout=max_time) fuzz_result = runner.fuzz( corpus_directories, fuzz_timeout=fuzz_timeout, additional_args=options.arguments, artifact_prefix=reproducers_dir, extra_env=options.extra_env, ) project_qualified_fuzzer_name = _project_qualified_fuzzer_name( target_path) dict_error_match = DICT_PARSING_FAILED_REGEX.search(fuzz_result.output) if dict_error_match: logs.log_error( "Dictionary parsing failed (target={target}, line={line}).". format(target=project_qualified_fuzzer_name, line=dict_error_match.group(1)), engine_output=fuzz_result.output, ) elif (not environment.get_value("USE_MINIJAIL") and fuzz_result.return_code == constants.LIBFUZZER_ERROR_EXITCODE): # Minijail returns 1 if the exit code is nonzero. # Otherwise: we can assume that a return code of 1 means that libFuzzer # itself ran into an error. logs.log_error( ENGINE_ERROR_MESSAGE + " (target={target}).".format( target=project_qualified_fuzzer_name), engine_output=fuzz_result.output, ) log_lines = utils.decode_to_unicode(fuzz_result.output).splitlines() # Output can be large, so save some memory by removing reference to the # original output which is no longer needed. fuzz_result.output = None # Check if we crashed, and get the crash testcase path. crash_testcase_file_path = runner.get_testcase_path(log_lines) # If we exited with a non-zero return code with no crash file in output from # libFuzzer, this is most likely a startup crash. Use an empty testcase to # to store it as a crash. if not crash_testcase_file_path and fuzz_result.return_code: crash_testcase_file_path = self._create_empty_testcase_file() # Parse stats information based on libFuzzer output. parsed_stats = libfuzzer.parse_log_stats(log_lines) # Extend parsed stats by additional performance features. parsed_stats.update( stats.parse_performance_features(log_lines, options.strategies, options.arguments)) # Set some initial stat overrides. timeout_limit = fuzzer_utils.extract_argument(options.arguments, constants.TIMEOUT_FLAG, remove=False) expected_duration = runner.get_max_total_time(fuzz_timeout) actual_duration = int(fuzz_result.time_executed) fuzzing_time_percent = 100 * actual_duration / float(expected_duration) parsed_stats.update({ "timeout_limit": int(timeout_limit), "expected_duration": expected_duration, "actual_duration": actual_duration, "fuzzing_time_percent": fuzzing_time_percent, }) # Remove fuzzing arguments before merge and dictionary analysis step. arguments = options.arguments[:] libfuzzer.remove_fuzzing_arguments(arguments) self._merge_new_units( target_path, options.corpus_dir, new_corpus_dir, options.fuzz_corpus_dirs, arguments, parsed_stats, ) fuzz_logs = "\n".join(log_lines) crashes = [] if crash_testcase_file_path: # Use higher timeout for reproduction. reproduce_arguments = arguments[:] libfuzzer.fix_timeout_argument_for_reproduction( reproduce_arguments) # Write the new testcase. # Copy crash testcase contents into the main testcase path. crashes.append( engine.Crash( crash_testcase_file_path, fuzz_logs, reproduce_arguments, actual_duration, )) libfuzzer.analyze_and_update_recommended_dictionary( runner, project_qualified_fuzzer_name, log_lines, options.corpus_dir, arguments, ) return engine.FuzzResult( fuzz_logs, fuzz_result.command, crashes, parsed_stats, fuzz_result.time_executed, )