| // Copyright 2022 The Centipede Authors. | |
| // | |
| // Licensed under the Apache License, Version 2.0 (the "License"); | |
| // you may not use this file except in compliance with the License. | |
| // You may obtain a copy of the License at | |
| // | |
| // https://www.apache.org/licenses/LICENSE-2.0 | |
| // | |
| // Unless required by applicable law or agreed to in writing, software | |
| // distributed under the License is distributed on an "AS IS" BASIS, | |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| // See the License for the specific language governing permissions and | |
| // limitations under the License. | |
| // Centipede: an experimental distributed fuzzing engine. | |
| // Very simple / naive so far. | |
| // Main use case: large out-of-process fuzz targets with relatively slow | |
| // execution (< 100 exec/s). | |
| // | |
| // Basic approach (subject to change): | |
| // * All state is stored in a local or remote directory `workdir`. | |
| // * State consists of a corpus (inputs) and feature sets (see feature_t). | |
| // * Feature sets are associated with a binary, so that two binaries | |
| // have independent feature sets stored in different subdirs in `workdir`, | |
| // like binaryA-sha1-of-A and binaryB-sha1-of-B. | |
| // If the binary is recompiled at different revision or with different | |
| // compiler options, it is a different binary and feature sets will need to be | |
| // recomputed for the new binary in its separate dir. | |
| // * The corpus is not tied to the binary. It is stored in `workdir`/. | |
| // * The fuzzer runs in `total_shards` independent processes. | |
| // * Each shard appends data to its own files in `workdir`: corpus and features; | |
| // no other process writes to those files. | |
| // * Each shard may periodically read some other shard's corpus and features. | |
| // Since all files are append-only (no renames, no deletions) we may only | |
| // have partial reads, and the algorithm is expected to tolerate those. | |
| // * Fuzzing can be run locally in multiple processes, with a local `workdir` | |
| // or on a cluster, which supports `workdir` on a remote file system. | |
| // * The intent is to scale to an arbitrary number of shards, | |
| // currently tested with total_shards = 10000. | |
| // | |
| // Differential fuzzing is not yet properly implemented. | |
| // Currently, one can run target A in a given workdir, then target B, and so | |
| // on, and the corpus will grow over time benefiting from all targets. | |
| #include "./centipede/centipede.h" | |
| #include <algorithm> | |
| #include <atomic> | |
| #include <cmath> | |
| #include <cstddef> | |
| #include <cstdint> | |
| #include <cstdlib> | |
| #include <filesystem> // NOLINT | |
| #include <functional> | |
| #include <iostream> | |
| #include <memory> | |
| #include <numeric> | |
| #include <sstream> | |
| #include <string> | |
| #include <string_view> | |
| #include <utility> | |
| #include <vector> | |
| #include "absl/base/attributes.h" | |
| #include "absl/base/const_init.h" // NOLINT | |
| #include "absl/base/nullability.h" | |
| #include "absl/container/flat_hash_set.h" | |
| #include "absl/status/status.h" | |
| #include "absl/strings/ascii.h" | |
| #include "absl/strings/str_cat.h" | |
| #include "absl/strings/str_format.h" | |
| #include "absl/strings/str_split.h" | |
| #include "absl/synchronization/mutex.h" | |
| #include "absl/time/clock.h" | |
| #include "absl/time/time.h" | |
| #include "absl/types/span.h" | |
| #include "./centipede/binary_info.h" | |
| #include "./centipede/centipede_callbacks.h" | |
| #include "./centipede/command.h" | |
| #include "./centipede/control_flow.h" | |
| #include "./centipede/corpus.h" | |
| #include "./centipede/corpus_io.h" | |
| #include "./centipede/coverage.h" | |
| #include "./centipede/environment.h" | |
| #include "./centipede/feature.h" | |
| #include "./centipede/feature_set.h" | |
| #include "./centipede/mutation_data.h" | |
| #include "./centipede/runner_result.h" | |
| #include "./centipede/rusage_profiler.h" | |
| #include "./centipede/rusage_stats.h" | |
| #include "./centipede/stats.h" | |
| #include "./centipede/stop.h" | |
| #include "./centipede/util.h" | |
| #include "./centipede/workdir.h" | |
| #include "./common/blob_file.h" | |
| #include "./common/defs.h" | |
| #include "./common/hash.h" | |
| #include "./common/logging.h" | |
| #include "./common/remote_file.h" | |
| #include "./common/status_macros.h" | |
| namespace fuzztest::internal { | |
| namespace { | |
| std::vector<MutantRef> InputsToMutantRefs(const std::vector<ByteSpan>& inputs) { | |
| std::vector<MutantRef> mutants; | |
| mutants.reserve(inputs.size()); | |
| for (const auto input : inputs) { | |
| mutants.push_back(MutantRef{input, Mutant::kOriginNone}); | |
| } | |
| return mutants; | |
| } | |
| void SaveReproducer(const WorkDir& wd, std::string_view log_prefix, | |
| size_t input_idx, ByteSpan reproducer, | |
| const BatchResult& result) { | |
| auto hash = Hash(reproducer); | |
| auto crash_dir = wd.CrashReproducerDirPaths().MyShard(); | |
| FUZZTEST_CHECK_OK(RemoteMkdir(crash_dir)); | |
| std::string input_file_path = std::filesystem::path(crash_dir) / hash; | |
| auto crash_metadata_dir = wd.CrashMetadataDirPaths().MyShard(); | |
| FUZZTEST_CHECK_OK(RemoteMkdir(crash_metadata_dir)); | |
| std::string crash_metadata_path_prefix = | |
| std::filesystem::path(crash_metadata_dir) / hash; | |
| FUZZTEST_LOG(INFO) | |
| << log_prefix << "Detected crash-reproducing input:" | |
| << "\nInput index : " << input_idx | |
| << "\nInput bytes : " << AsPrintableString(reproducer, /*max_len=*/32) | |
| << "\nExit code : " << result.exit_code() | |
| << "\nFailure : " << result.failure_description() | |
| << "\nSignature : " | |
| << AsPrintableString(AsByteSpan(result.failure_signature()), | |
| /*max_len=*/32) | |
| << "\nSaving input to: " << input_file_path << "\nSaving crash" // | |
| << "\nmetadata to : " << crash_metadata_path_prefix << ".*"; | |
| FUZZTEST_CHECK_OK(RemoteFileSetContents(input_file_path, reproducer)); | |
| FUZZTEST_CHECK_OK( | |
| RemoteFileSetContents(absl::StrCat(crash_metadata_path_prefix, ".desc"), | |
| result.failure_description())); | |
| FUZZTEST_CHECK_OK( | |
| RemoteFileSetContents(absl::StrCat(crash_metadata_path_prefix, ".sig"), | |
| result.failure_signature())); | |
| } | |
| } // namespace | |
| Centipede::Centipede(const Environment& env, CentipedeCallbacks& user_callbacks, | |
| const BinaryInfo& binary_info, | |
| CoverageLogger& coverage_logger, std::atomic<Stats>& stats, | |
| StopCondition& stop_condition) | |
| : env_(env), | |
| stop_condition_(stop_condition), | |
| user_callbacks_(user_callbacks), | |
| rng_(env_.seed), | |
| // TODO(kcc): [impl] find a better way to compute frequency_threshold. | |
| fs_(env_.feature_frequency_threshold, env_.MakeDomainDiscardMask()), | |
| corpus_([this] { | |
| const auto parsed_weight_method = | |
| Corpus::ParseWeightMethod(env_.corpus_weight_method); | |
| FUZZTEST_CHECK(parsed_weight_method.has_value()) | |
| << "Unknown corpus weight method " << env_.corpus_weight_method; | |
| return *parsed_weight_method; | |
| }()), | |
| coverage_frontier_(binary_info), | |
| binary_info_(binary_info), | |
| pc_table_(binary_info_.pc_table), | |
| symbols_(binary_info_.symbols), | |
| function_filter_(env_.function_filter, symbols_), | |
| coverage_logger_(coverage_logger), | |
| stats_(stats), | |
| input_filter_path_(std::filesystem::path(TemporaryLocalDirPath()) | |
| .append("filter-input")), | |
| input_filter_cmd_{[&] { | |
| Command::Options cmd_options; | |
| cmd_options.args = {input_filter_path_}; | |
| cmd_options.stdout_file_prefix = | |
| std::filesystem::path(TemporaryLocalDirPath()) | |
| .append("filter-input.log"); | |
| cmd_options.stderr_file_prefix = cmd_options.stdout_file_prefix; | |
| return Command{env_.input_filter, std::move(cmd_options)}; | |
| }()}, | |
| rusage_profiler_( | |
| /*scope=*/RUsageScope::ThisProcess(), | |
| /*metrics=*/env.DumpRUsageTelemetryInThisShard() | |
| ? RUsageProfiler::kAllMetrics | |
| : RUsageProfiler::kMetricsOff, | |
| /*raii_actions=*/RUsageProfiler::kRaiiOff, | |
| /*location=*/{__FILE__, __LINE__}, | |
| /*description=*/"Engine") { | |
| FUZZTEST_CHECK(env_.seed) << "env_.seed must not be zero"; | |
| if (!env_.input_filter.empty() && env_.fork_server) | |
| input_filter_cmd_.StartForkServer(TemporaryLocalDirPath(), "input_filter"); | |
| } | |
| void Centipede::CorpusToFiles(const Environment &env, std::string_view dir) { | |
| std::vector<std::string> sharded_corpus_files; | |
| FUZZTEST_CHECK_OK(RemoteGlobMatch( | |
| WorkDir{env}.CorpusFilePaths().AllShardsGlob(), sharded_corpus_files)); | |
| ExportCorpus(sharded_corpus_files, dir); | |
| } | |
| void Centipede::CorpusFromFiles(const Environment &env, std::string_view dir) { | |
| // Shard the file paths in the source `dir` based on hashes of filenames. | |
| // Such partition is stable: a given file always goes to a specific shard. | |
| std::vector<std::vector<std::string>> sharded_paths(env.total_shards); | |
| std::vector<std::string> paths; | |
| size_t total_paths = 0; | |
| const std::vector<std::string> listed_paths = | |
| ValueOrDie(RemoteListFiles(dir, /*recursively=*/true)); | |
| for (const std::string &path : listed_paths) { | |
| size_t filename_hash = std::hash<std::string>{}(path); | |
| sharded_paths[filename_hash % env.total_shards].push_back(path); | |
| ++total_paths; | |
| } | |
| // If the destination `workdir` is specified (note that empty means "use the | |
| // current directory"), we might need to create it. | |
| if (!env.workdir.empty()) { | |
| FUZZTEST_CHECK_OK(RemoteMkdir(env.workdir)); | |
| } | |
| // Iterate over all shards, adding inputs to the current shard. | |
| size_t inputs_added = 0; | |
| size_t inputs_ignored = 0; | |
| const auto corpus_file_paths = WorkDir{env}.CorpusFilePaths(); | |
| for (size_t shard = 0; shard < env.total_shards; shard++) { | |
| const std::string corpus_file_path = corpus_file_paths.Shard(shard); | |
| size_t num_shard_bytes = 0; | |
| // Read the shard (if it exists), collect input hashes from it. | |
| absl::flat_hash_set<std::string> existing_hashes; | |
| if (RemotePathExists(corpus_file_path)) { | |
| auto reader = DefaultBlobFileReaderFactory(); | |
| // May fail to open if file doesn't exist. | |
| reader->Open(corpus_file_path).IgnoreError(); | |
| ByteSpan blob; | |
| while (reader->Read(blob).ok()) { | |
| existing_hashes.insert(Hash(blob)); | |
| } | |
| } | |
| // Add inputs to the current shard, if the shard doesn't have them already. | |
| auto appender = DefaultBlobFileWriterFactory(env.riegeli); | |
| FUZZTEST_CHECK_OK(appender->Open(corpus_file_path, "a")) | |
| << "Failed to open corpus file: " << corpus_file_path; | |
| ByteArray shard_data; | |
| for (const auto &path : sharded_paths[shard]) { | |
| std::string input; | |
| FUZZTEST_CHECK_OK(RemoteFileGetContents(path, input)); | |
| if (input.empty() || existing_hashes.contains(Hash(input))) { | |
| ++inputs_ignored; | |
| continue; | |
| } | |
| FUZZTEST_CHECK_OK(appender->Write(ByteArray{input.begin(), input.end()})); | |
| ++inputs_added; | |
| } | |
| FUZZTEST_LOG(INFO) << VV(shard) << VV(inputs_added) << VV(inputs_ignored) | |
| << VV(num_shard_bytes) << VV(shard_data.size()); | |
| } | |
| FUZZTEST_CHECK_EQ(total_paths, inputs_added + inputs_ignored); | |
| } | |
| absl::Status Centipede::CrashesToFiles(const Environment &env, | |
| std::string_view dir) { | |
| std::vector<std::string> reproducer_dirs; | |
| const auto wd = WorkDir{env}; | |
| auto reproducer_match_status = RemoteGlobMatch( | |
| wd.CrashReproducerDirPaths().AllShardsGlob(), reproducer_dirs); | |
| if (!reproducer_match_status.ok() && | |
| !absl::IsNotFound(reproducer_match_status)) { | |
| return reproducer_match_status; | |
| } | |
| absl::flat_hash_set<std::string> crash_ids; | |
| for (const auto &reproducer_dir : reproducer_dirs) { | |
| ASSIGN_OR_RETURN_IF_NOT_OK( | |
| std::vector<std::string> reproducer_paths, | |
| RemoteListFiles(reproducer_dir, /*recursively=*/false)); | |
| for (const auto &reproducer_path : reproducer_paths) { | |
| std::string id = std::filesystem::path{reproducer_path}.filename(); | |
| if (auto [_it, inserted] = crash_ids.insert(id); !inserted) { | |
| continue; | |
| } | |
| RETURN_IF_NOT_OK(RemoteFileCopy( | |
| reproducer_path, | |
| (std::filesystem::path{dir} / absl::StrCat(id, ".data")).string())); | |
| const auto shard_index = wd.CrashReproducerDirPaths().GetShardIndex( | |
| std::filesystem::path{reproducer_path}.parent_path().string()); | |
| FUZZTEST_CHECK(shard_index.has_value()); | |
| const auto metadata_dir = wd.CrashMetadataDirPaths().Shard(*shard_index); | |
| const auto description_filename = absl::StrCat(id, ".desc"); | |
| const auto signature_filename = absl::StrCat(id, ".sig"); | |
| RETURN_IF_NOT_OK(RemoteFileCopy( | |
| (std::filesystem::path{metadata_dir} / description_filename).string(), | |
| (std::filesystem::path{dir} / description_filename).string())); | |
| RETURN_IF_NOT_OK(RemoteFileCopy( | |
| (std::filesystem::path{metadata_dir} / signature_filename).string(), | |
| (std::filesystem::path{dir} / signature_filename).string())); | |
| } | |
| } | |
| return absl::OkStatus(); | |
| } | |
| void Centipede::UpdateAndMaybeLogStats(std::string_view log_type, | |
| size_t min_log_level) { | |
| // `fuzz_start_time_ == ` means that fuzzing hasn't started yet. If so, grab | |
| // the baseline numbers. | |
| const double fuzz_time_secs = | |
| fuzz_start_time_ == absl::InfiniteFuture() | |
| ? 0 | |
| : absl::ToDoubleSeconds(absl::Now() - fuzz_start_time_); | |
| const double execs_per_sec = | |
| fuzz_time_secs == 0 ? 0 : (1.0 * num_runs_ / fuzz_time_secs); | |
| const auto [max_corpus_size, avg_corpus_size] = corpus_.MaxAndAvgSize(); | |
| // NOTE: For now, this will double-count rusage in every shard on the same | |
| // machine. The stats reporter knows and deals with that. | |
| static const auto rusage_scope = RUsageScope::ThisProcess(); | |
| const auto rusage_timing = RUsageTiming::Snapshot(rusage_scope); | |
| const auto rusage_memory = RUsageMemory::Snapshot(rusage_scope); | |
| namespace fd = feature_domains; | |
| stats_.store(Stats{ | |
| StatsMeta{ | |
| /*timestamp_unix_micros=*/ | |
| static_cast<uint64_t>(absl::ToUnixMicros(absl::Now())), | |
| }, | |
| ExecStats{ | |
| /*fuzz_time_sec=*/static_cast<uint64_t>(std::ceil(fuzz_time_secs)), | |
| /*num_executions*/ num_runs_, | |
| /*num_target_crashes*/ static_cast<uint64_t>(num_crashes_), | |
| }, | |
| CovStats{ | |
| /*num_covered_pcs=*/fs_.CountFeatures(fd::kPCs), | |
| /*num_8bit_counter_features=*/fs_.CountFeatures(fd::k8bitCounters), | |
| /*num_data_flow_features=*/fs_.CountFeatures(fd::kDataFlow), | |
| /*num_cmp_features=*/fs_.CountFeatures(fd::kCMPDomains), | |
| /*num_call_stack_features=*/fs_.CountFeatures(fd::kCallStack), | |
| /*num_bounded_path_features=*/fs_.CountFeatures(fd::kBoundedPath), | |
| /*num_pc_pair_features=*/fs_.CountFeatures(fd::kPCPair), | |
| /*num_user_features=*/fs_.CountFeatures(fd::kUserDomains), | |
| /*num_user0_features=*/fs_.CountFeatures(fd::kUserDomains[0]), | |
| /*num_user1_features=*/fs_.CountFeatures(fd::kUserDomains[1]), | |
| /*num_user2_features=*/fs_.CountFeatures(fd::kUserDomains[2]), | |
| /*num_user3_features=*/fs_.CountFeatures(fd::kUserDomains[3]), | |
| /*num_user4_features=*/fs_.CountFeatures(fd::kUserDomains[4]), | |
| /*num_user5_features=*/fs_.CountFeatures(fd::kUserDomains[5]), | |
| /*num_user6_features=*/fs_.CountFeatures(fd::kUserDomains[6]), | |
| /*num_user7_features=*/fs_.CountFeatures(fd::kUserDomains[7]), | |
| /*num_user8_features=*/fs_.CountFeatures(fd::kUserDomains[8]), | |
| /*num_user9_features=*/fs_.CountFeatures(fd::kUserDomains[9]), | |
| /*num_user10_features=*/fs_.CountFeatures(fd::kUserDomains[10]), | |
| /*num_user11_features=*/fs_.CountFeatures(fd::kUserDomains[11]), | |
| /*num_user12_features=*/fs_.CountFeatures(fd::kUserDomains[12]), | |
| /*num_user13_features=*/fs_.CountFeatures(fd::kUserDomains[13]), | |
| /*num_user14_features=*/fs_.CountFeatures(fd::kUserDomains[14]), | |
| /*num_user15_features=*/fs_.CountFeatures(fd::kUserDomains[15]), | |
| /*num_unknown_features=*/fs_.CountFeatures(fd::kUnknown), | |
| /*num_funcs_in_frontier=*/coverage_frontier_.NumFunctionsInFrontier(), | |
| }, | |
| CorpusStats{ | |
| /*active_corpus_size=*/corpus_.NumActive(), | |
| /*total_corpus_size=*/corpus_.NumTotal(), | |
| /*max_corpus_element_size=*/max_corpus_size, | |
| /*avg_corpus_element_size=*/avg_corpus_size, | |
| }, | |
| RusageStats{ | |
| /*engine_rusage_avg_millicores=*/static_cast<uint64_t>( | |
| std::lround(rusage_timing.cpu_hyper_cores * 1000)), | |
| /*engine_rusage_cpu_percent=*/ | |
| static_cast<uint64_t>( | |
| std::lround(rusage_timing.cpu_utilization * 100)), | |
| /*engine_rusage_rss_mb=*/ | |
| static_cast<uint64_t>(rusage_memory.mem_rss >> 20), | |
| /*engine_rusage_vsize_mb=*/ | |
| static_cast<uint64_t>(rusage_memory.mem_vsize >> 20), | |
| }, | |
| }); | |
| if (env_.log_level < min_log_level) return; | |
| std::ostringstream os; | |
| auto LogIfNotZero = [&os](size_t value, std::string_view name) { | |
| if (!value) return; | |
| os << " " << name << ": " << value; | |
| }; | |
| if (!env_.experiment_name.empty()) os << env_.experiment_name << " "; | |
| os << "[S" << env_.my_shard_index << "." << num_runs_ << "] " << log_type | |
| << ": "; | |
| os << fs_; | |
| os << " corp: " << corpus_.NumActive() << "/" << corpus_.NumTotal(); | |
| LogIfNotZero(coverage_frontier_.NumFunctionsInFrontier(), "fr"); | |
| LogIfNotZero(num_crashes_, "crash"); | |
| os << " max/avg: " << max_corpus_size << "/" << avg_corpus_size << " " | |
| << corpus_.MemoryUsageString(); | |
| os << " exec/s: " | |
| << (execs_per_sec < 1.0 ? execs_per_sec : std::round(execs_per_sec)); | |
| os << " ctrl-mb: " << (rusage_memory.mem_rss >> 20); | |
| os << " exec-mb: " << execution_peak_rss_mb_; | |
| FUZZTEST_LOG(INFO) << os.str(); | |
| } | |
| void Centipede::LogFeaturesAsSymbols(const FeatureVec &fv) { | |
| if (!env_.LogFeaturesInThisShard()) return; | |
| for (auto feature : fv) { | |
| if (!feature_domains::kPCs.Contains(feature)) continue; | |
| PCIndex pc_index = ConvertPCFeatureToPcIndex(feature); | |
| auto description = coverage_logger_.ObserveAndDescribeIfNew(pc_index); | |
| if (description.empty()) continue; | |
| FUZZTEST_LOG(INFO) << description; | |
| } | |
| } | |
| bool Centipede::InputPassesFilter(ByteSpan input) { | |
| if (env_.input_filter.empty()) return true; | |
| WriteToLocalFile(input_filter_path_, input); | |
| bool result = input_filter_cmd_.Execute(&stop_condition_) == EXIT_SUCCESS; | |
| std::filesystem::remove(input_filter_path_); | |
| return result; | |
| } | |
| bool Centipede::ExecuteAndReportCrash(std::string_view binary, | |
| absl::Span<const ByteSpan> input_vec, | |
| BatchResult& batch_result) { | |
| bool success = user_callbacks_.Execute(binary, input_vec, batch_result); | |
| if (success) return true; | |
| if (batch_result.IsIgnoredFailure()) { | |
| FUZZTEST_LOG(INFO) << "Skip reporting " | |
| << batch_result.failure_description(); | |
| return true; | |
| } | |
| const bool can_report_without_reexecution = | |
| input_vec.size() == 1 && !env_.persistent_mode && | |
| batch_result.IsInputFailure() && !stop_condition_.StopRequested() && | |
| batch_result.failure_description() != kExecutionFailurePerBatchTimeout; | |
| if (stop_condition_.ShouldStop() && !can_report_without_reexecution) { | |
| FUZZTEST_LOG_FIRST_N(WARNING, 1) | |
| << "Crash found but the stop condition is met - not reporting further " | |
| "possibly related crashes."; | |
| return true; | |
| } | |
| ReportCrash(binary, input_vec, batch_result); | |
| return false; | |
| } | |
| // *** Highly experimental and risky. May not scale well for large targets. *** | |
| // | |
| // The idea: an unordered pair of two features {a, b} is by itself a feature. | |
| // In the worst case, the number of such synthetic features is a square of | |
| // the number of regular features, which may not scale. | |
| // For now, we only treat pairs of PCs as features, which is still quadratic | |
| // by the number of PCs. But in moderate-sized programs this may be tolerable. | |
| // | |
| // Rationale: if two different parts of the target are exercised simultaneously, | |
| // this may create interesting behaviour that is hard to capture with regular | |
| // control flow (or other) features. | |
| size_t Centipede::AddPcPairFeatures(FeatureVec &fv) { | |
| // Using a scratch vector to avoid allocations. | |
| auto &pcs = add_pc_pair_scratch_; | |
| pcs.clear(); | |
| size_t num_pcs = pc_table_.size(); | |
| size_t num_added_pairs = 0; | |
| // Collect PCs from fv. | |
| for (auto feature : fv) { | |
| if (feature_domains::kPCs.Contains(feature)) | |
| pcs.push_back(ConvertPCFeatureToPcIndex(feature)); | |
| } | |
| // The quadratic loop: iterate all PC pairs (!!). | |
| for (size_t i = 0, n = pcs.size(); i < n; ++i) { | |
| size_t pc1 = pcs[i]; | |
| for (size_t j = i + 1; j < n; ++j) { | |
| size_t pc2 = pcs[j]; | |
| feature_t f = feature_domains::kPCPair.ConvertToMe( | |
| ConvertPcPairToNumber(pc1, pc2, num_pcs)); | |
| // If we have seen this pair at least once, ignore it. | |
| if (fs_.Frequency(f) != 0) continue; | |
| fv.push_back(f); | |
| ++num_added_pairs; | |
| } | |
| } | |
| return num_added_pairs; | |
| } | |
| bool Centipede::RunBatch( | |
| absl::Span<const MutantRef> mutants, | |
| BlobFileWriter* absl_nullable corpus_file, | |
| BlobFileWriter* absl_nullable features_file, | |
| BlobFileWriter* absl_nullable unconditional_features_file) { | |
| BatchResult batch_result; | |
| std::vector<ByteSpan> inputs; | |
| inputs.reserve(mutants.size()); | |
| for (auto mutant : mutants) { | |
| inputs.push_back(mutant.data); | |
| } | |
| bool success = ExecuteAndReportCrash(env_.binary, inputs, batch_result); | |
| FUZZTEST_CHECK_EQ(mutants.size(), batch_result.results().size()); | |
| for (const auto &extra_binary : env_.extra_binaries) { | |
| if (stop_condition_.ShouldStop()) break; | |
| BatchResult extra_batch_result; | |
| success = ExecuteAndReportCrash(extra_binary, inputs, extra_batch_result) && | |
| success; | |
| } | |
| if (stop_condition_.StopRequested()) return false; | |
| if (!success && env_.exit_on_crash) { | |
| FUZZTEST_LOG(INFO) << "--exit_on_crash is enabled; exiting soon"; | |
| stop_condition_.RequestStop(EXIT_FAILURE, | |
| "A crash was found in the test with " | |
| "--exit_on_crash set for the engine"); | |
| return false; | |
| } | |
| bool batch_gained_new_coverage = false; | |
| for (size_t i = 0; i < mutants.size(); i++) { | |
| if (stop_condition_.ShouldStop()) break; | |
| FeatureVec &fv = batch_result.results()[i].mutable_features(); | |
| bool function_filter_passed = function_filter_.filter(fv); | |
| bool input_gained_new_coverage = fs_.PruneFeaturesAndCountUnseen(fv) != 0; | |
| if (env_.use_pcpair_features && AddPcPairFeatures(fv) != 0) | |
| input_gained_new_coverage = true; | |
| bool canonicalized = false; | |
| if (unconditional_features_file != nullptr) { | |
| fs_.CanonicalizeFeatures(fv); | |
| canonicalized = true; | |
| FUZZTEST_CHECK_OK(unconditional_features_file->Write( | |
| PackFeaturesAndHash(inputs[i], fv))); | |
| } | |
| const auto& result = batch_result.results()[i]; | |
| const bool may_reduce = | |
| mutants[i].origin != Mutant::kOriginNone && env_.reduce_inputs && | |
| // Consider mutator-provided signals for smaller-ness. | |
| inputs[i].size() < corpus_.Records()[mutants[i].origin].data.size(); | |
| if (!input_gained_new_coverage && !may_reduce) continue; | |
| // TODO: [impl] add stats for filtered-out inputs. | |
| if (!InputPassesFilter(inputs[i])) continue; | |
| // Need `fv` to be canonicalized at this point. | |
| if (!canonicalized) { | |
| fs_.CanonicalizeFeatures(fv); | |
| } | |
| bool write_to_files = false; | |
| if (input_gained_new_coverage) { | |
| fs_.MergeFeatures(fv); | |
| LogFeaturesAsSymbols(fv); | |
| batch_gained_new_coverage = true; | |
| FUZZTEST_CHECK_GT(fv.size(), 0UL); | |
| if (function_filter_passed) { | |
| corpus_.Add(inputs[i], fv, result.metadata(), result.stats(), fs_, | |
| coverage_frontier_); | |
| } | |
| write_to_files = true; | |
| } else if (may_reduce && | |
| corpus_.TryReduceInput(mutants[i].origin, inputs[i], fv, | |
| result.metadata(), result.stats())) { | |
| write_to_files = true; | |
| } | |
| if (write_to_files) { | |
| if (corpus_file != nullptr) { | |
| FUZZTEST_CHECK_OK(corpus_file->Write(inputs[i])); | |
| } | |
| if (features_file != nullptr) { | |
| FUZZTEST_CHECK_OK( | |
| features_file->Write(PackFeaturesAndHash(inputs[i], fv))); | |
| } | |
| if (!env_.corpus_dir.empty() && !env_.corpus_dir[0].empty()) { | |
| WriteToLocalHashedFileInDir(env_.corpus_dir[0], inputs[i]); | |
| } | |
| } | |
| } | |
| num_runs_ += batch_result.num_outputs_read(); | |
| if (batch_result.num_outputs_read() > 0) { | |
| // The reported values increase monotonically within a batch. So it suffices | |
| // to look at the last value. | |
| execution_peak_rss_mb_ = | |
| std::max(execution_peak_rss_mb_, | |
| batch_result.results()[batch_result.num_outputs_read() - 1] | |
| .stats() | |
| .peak_rss_mb); | |
| } | |
| corpus_.UpdateWeights(fs_, coverage_frontier_, env_.exec_time_weight_scaling); | |
| return batch_gained_new_coverage; | |
| } | |
| // TODO(kcc): [impl] don't reread the same corpus twice. | |
| void Centipede::LoadShard(const Environment &load_env, size_t shard_index, | |
| bool rerun) { | |
| FUZZTEST_VLOG(1) << "Loading shard " << shard_index | |
| << (rerun ? " with rerunning" : " without rerunning"); | |
| size_t num_added_inputs = 0; | |
| size_t num_skipped_inputs = 0; | |
| std::vector<ByteArray> inputs_to_rerun; | |
| auto input_features_callback = [&](ByteArray input, | |
| FeatureVec input_features) { | |
| if (stop_condition_.ShouldStop()) return; | |
| if (input_features.empty()) { | |
| if (rerun) { | |
| inputs_to_rerun.emplace_back(std::move(input)); | |
| } | |
| } else { | |
| LogFeaturesAsSymbols(input_features); | |
| const auto num_new_features = | |
| fs_.PruneFeaturesAndCountUnseen(input_features); | |
| if (num_new_features != 0) { | |
| FUZZTEST_VLOG(10) << "Adding input " << Hash(input) | |
| << "; new features: " << num_new_features; | |
| fs_.MergeFeatures(input_features); | |
| // TODO(xinhaoyuan): metadata and stats are currently not saved to disk | |
| // and not reloaded. | |
| corpus_.Add(input, input_features, {}, {}, fs_, coverage_frontier_); | |
| ++num_added_inputs; | |
| } else { | |
| FUZZTEST_VLOG(10) << "Skipping input: " << Hash(input); | |
| ++num_skipped_inputs; | |
| } | |
| } | |
| }; | |
| // See serialize_shard_loads on why we may want to serialize shard loads. | |
| // TODO(kcc): remove serialize_shard_loads when LoadShards() uses less RAM. | |
| const WorkDir wd{load_env}; | |
| const std::string corpus_path = wd.CorpusFilePaths().Shard(shard_index); | |
| const std::string features_path = wd.FeaturesFilePaths().Shard(shard_index); | |
| if (env_.serialize_shard_loads) { | |
| ABSL_CONST_INIT static absl::Mutex load_shard_mu{absl::kConstInit}; | |
| absl::MutexLock lock(load_shard_mu); | |
| ReadShard(corpus_path, features_path, input_features_callback); | |
| } else { | |
| ReadShard(corpus_path, features_path, input_features_callback); | |
| } | |
| FUZZTEST_VLOG(1) << "Loaded shard " << shard_index << ": added " | |
| << num_added_inputs << " / skipped " << num_skipped_inputs | |
| << " inputs"; | |
| if (num_added_inputs > 0) UpdateAndMaybeLogStats("load-shard", 1); | |
| if (!inputs_to_rerun.empty()) Rerun(inputs_to_rerun); | |
| } | |
| void Centipede::LoadAllShardsInRandomOrder(const Environment &load_env, | |
| bool rerun_my_shard) { | |
| // TODO(ussuri): It seems logical to reset `corpus_` before this, but | |
| // that broke `ShardsAndDistillTest` in testing/centipede_test.cc. | |
| // Investigate. | |
| std::vector<size_t> shard_idxs(env_.total_shards); | |
| std::iota(shard_idxs.begin(), shard_idxs.end(), 0); | |
| std::shuffle(shard_idxs.begin(), shard_idxs.end(), rng_); | |
| size_t num_shards_loaded = 0; | |
| for (size_t shard_idx : shard_idxs) { | |
| const bool rerun = rerun_my_shard && shard_idx == env_.my_shard_index; | |
| LoadShard(load_env, shard_idx, rerun); | |
| FUZZTEST_LOG_IF(INFO, (++num_shards_loaded % 100) == 0) | |
| << VV(num_shards_loaded); | |
| } | |
| } | |
| void Centipede::Rerun(std::vector<ByteArray> &to_rerun) { | |
| if (to_rerun.empty()) return; | |
| auto features_file_path = wd_.FeaturesFilePaths().Shard(env_.my_shard_index); | |
| auto features_file = DefaultBlobFileWriterFactory(env_.riegeli); | |
| FUZZTEST_CHECK_OK(features_file->Open(features_file_path, "a")); | |
| const int num_attempts = std::max(1, env_.replay_coverage_attempts); | |
| FUZZTEST_LOG(INFO) << to_rerun.size() << " inputs to rerun across " | |
| << num_attempts << " attempt(s)"; | |
| // Re-run all inputs for which we don't know their features. | |
| // Run in batches of at most env_.batch_size inputs each. | |
| for (int attempt = 0; attempt < num_attempts; ++attempt) { | |
| if (stop_condition_.ShouldStop()) break; | |
| BlobFileWriter* writer = (attempt == 0) ? features_file.get() : nullptr; | |
| for (size_t i = 0; i < to_rerun.size(); i += env_.batch_size) { | |
| if (stop_condition_.ShouldStop()) break; | |
| const size_t batch_size = std::min(to_rerun.size() - i, env_.batch_size); | |
| if (RunBatch(InputsToMutantRefs({to_rerun.begin() + i, | |
| to_rerun.begin() + i + batch_size}), | |
| nullptr, nullptr, writer)) { | |
| UpdateAndMaybeLogStats("rerun-old", 1); | |
| } | |
| } | |
| } | |
| to_rerun.clear(); | |
| } | |
| void Centipede::GenerateCoverageReport(std::string_view filename_annotation, | |
| std::string_view description) { | |
| if (pc_table_.empty()) return; | |
| auto coverage_path = wd_.CoverageReportPath(filename_annotation); | |
| FUZZTEST_LOG(INFO) << "Generate coverage report [" << description << "]; " | |
| << VV(coverage_path); | |
| auto pci_vec = fs_.ToCoveragePCs(); | |
| Coverage coverage(pc_table_, pci_vec); | |
| coverage.DumpReportToFile(symbols_, coverage_path, description); | |
| } | |
| void Centipede::GenerateCorpusStats(std::string_view filename_annotation, | |
| std::string_view description) { | |
| auto stats_path = wd_.CorpusStatsPath(filename_annotation); | |
| FUZZTEST_LOG(INFO) << "Generate corpus stats [" << description << "]; " | |
| << VV(stats_path); | |
| corpus_.DumpStatsToFile(fs_, stats_path, description); | |
| } | |
| // TODO(nedwill): add integration test once tests are refactored per b/255660879 | |
| void Centipede::GenerateSourceBasedCoverageReport( | |
| std::string_view filename_annotation, std::string_view description) { | |
| if (env_.clang_coverage_binary.empty()) return; | |
| auto report_path = wd_.SourceBasedCoverageReportPath(filename_annotation); | |
| FUZZTEST_LOG(INFO) << "Generate source based coverage report [" << description | |
| << "]; " << VV(report_path); | |
| FUZZTEST_CHECK_OK(RemoteMkdir(report_path)); | |
| std::vector<std::string> raw_profiles = wd_.EnumerateRawCoverageProfiles(); | |
| if (raw_profiles.empty()) { | |
| FUZZTEST_LOG(ERROR) << "No raw profiles found for coverage report"; | |
| return; | |
| } | |
| std::string indexed_profile_path = | |
| wd_.SourceBasedCoverageIndexedProfilePath(); | |
| std::vector<std::string> merge_arguments = {"merge", "-o", | |
| indexed_profile_path, "-sparse"}; | |
| for (const std::string &raw_profile : raw_profiles) { | |
| merge_arguments.push_back(raw_profile); | |
| } | |
| Command::Options merge_cmd_options; | |
| merge_cmd_options.args = std::move(merge_arguments); | |
| Command merge_command{"llvm-profdata", std::move(merge_cmd_options)}; | |
| if (merge_command.Execute() != EXIT_SUCCESS) { | |
| FUZZTEST_LOG(ERROR) << "Failed to run command " << merge_command.ToString(); | |
| return; | |
| } | |
| Command::Options generate_report_cmd_options; | |
| generate_report_cmd_options.args = { | |
| "show", "-format=html", absl::StrCat("-output-dir=", report_path), | |
| absl::StrCat("-instr-profile=", indexed_profile_path), | |
| env_.clang_coverage_binary}; | |
| Command generate_report_command{"llvm-cov", | |
| std::move(generate_report_cmd_options)}; | |
| if (generate_report_command.Execute() != EXIT_SUCCESS) { | |
| FUZZTEST_LOG(ERROR) << "Failed to run command " | |
| << generate_report_command.ToString(); | |
| return; | |
| } | |
| } | |
| void Centipede::GenerateRUsageReport(std::string_view filename_annotation, | |
| std::string_view description) { | |
| class ReportDumper : public RUsageProfiler::ReportSink { | |
| public: | |
| explicit ReportDumper(std::string_view path) | |
| : file_{*RemoteFileOpen(path, "w")} { | |
| FUZZTEST_CHECK(file_ != nullptr) << VV(path); | |
| FUZZTEST_CHECK_OK( | |
| RemoteFileSetWriteBufferSize(file_, 10UL * 1024 * 1024)); | |
| } | |
| ~ReportDumper() override { FUZZTEST_CHECK_OK(RemoteFileClose(file_)); } | |
| ReportDumper &operator<<(std::string_view fragment) override { | |
| FUZZTEST_CHECK_OK(RemoteFileAppend( | |
| file_, ByteArray{fragment.cbegin(), fragment.cend()})); | |
| return *this; | |
| } | |
| private: | |
| RemoteFile *file_; | |
| }; | |
| const auto &snapshot = rusage_profiler_.TakeSnapshot( | |
| {__FILE__, __LINE__}, std::string{description}); | |
| FUZZTEST_VLOG(1) << "Rusage @ " << description << ": " | |
| << snapshot.ShortMetricsStr(); | |
| auto path = wd_.RUsageReportPath(filename_annotation); | |
| FUZZTEST_LOG(INFO) << "Generate rusage report [" << description << "]; " | |
| << VV(env_.my_shard_index) << VV(path); | |
| ReportDumper dumper{path}; | |
| rusage_profiler_.GenerateReport(&dumper); | |
| } | |
| void Centipede::MaybeGenerateTelemetry(std::string_view filename_annotation, | |
| std::string_view description) { | |
| if (env_.DumpCorpusTelemetryInThisShard()) { | |
| GenerateCoverageReport(filename_annotation, description); | |
| GenerateCorpusStats(filename_annotation, description); | |
| GenerateSourceBasedCoverageReport(filename_annotation, description); | |
| } | |
| if (env_.DumpRUsageTelemetryInThisShard()) { | |
| GenerateRUsageReport(filename_annotation, description); | |
| } | |
| } | |
| void Centipede::MaybeGenerateTelemetryAfterBatch( | |
| std::string_view filename_annotation, size_t batch_index) { | |
| if (env_.DumpTelemetryForThisBatch(batch_index)) { | |
| MaybeGenerateTelemetry( // | |
| filename_annotation, absl::StrCat("After batch ", batch_index)); | |
| } | |
| } | |
| void Centipede::MergeFromOtherCorpus(std::string_view merge_from_dir, | |
| size_t shard_index_to_merge) { | |
| FUZZTEST_LOG(INFO) << __func__ << ": " << merge_from_dir; | |
| Environment merge_from_env = env_; | |
| merge_from_env.workdir = merge_from_dir; | |
| size_t initial_corpus_size = corpus_.NumActive(); | |
| LoadShard(merge_from_env, shard_index_to_merge, /*rerun=*/true); | |
| size_t new_corpus_size = corpus_.NumActive(); | |
| FUZZTEST_CHECK_GE(new_corpus_size, | |
| initial_corpus_size); // Corpus can't shrink here. | |
| if (new_corpus_size > initial_corpus_size) { | |
| auto appender = DefaultBlobFileWriterFactory(env_.riegeli); | |
| FUZZTEST_CHECK_OK( | |
| appender->Open(wd_.CorpusFilePaths().Shard(env_.my_shard_index), "a")); | |
| for (size_t idx = initial_corpus_size; idx < new_corpus_size; ++idx) { | |
| FUZZTEST_CHECK_OK(appender->Write(corpus_.Get(idx))); | |
| } | |
| FUZZTEST_LOG(INFO) << "Merge: " << (new_corpus_size - initial_corpus_size) | |
| << " new inputs added"; | |
| } | |
| } | |
| void Centipede::ReloadAllShardsAndWriteDistilledCorpus() { | |
| // Reload the shards. This automatically distills the corpus by discarding | |
| // inputs with duplicate feature sets as they are being added. Reloading | |
| // randomly leaves random winners from such sets of duplicates in the | |
| // distilled output: so multiple distilling shards will produce different | |
| // outputs from the same inputs (the property that we want). | |
| LoadAllShardsInRandomOrder(env_, /*rerun_my_shard=*/false); | |
| // Save the distilled corpus to a file in workdir and possibly to a hashed | |
| // file in the first corpus dir passed in `--corpus_dir`. | |
| const auto distill_to_path = wd_.DistilledCorpusFilePaths().MyShard(); | |
| FUZZTEST_LOG(INFO) << "Distilling: shard: " << env_.my_shard_index | |
| << " output: " << distill_to_path << " " | |
| << " distilled size: " << corpus_.NumActive(); | |
| const auto appender = DefaultBlobFileWriterFactory(env_.riegeli); | |
| // NOTE: Always overwrite distilled corpus files -- never append, unlike | |
| // "regular", per-shard corpus files. | |
| FUZZTEST_CHECK_OK(appender->Open(distill_to_path, "w")); | |
| for (size_t i = 0; i < corpus_.NumActive(); ++i) { | |
| const ByteArray &input = corpus_.Get(i); | |
| FUZZTEST_CHECK_OK(appender->Write(input)); | |
| if (!env_.corpus_dir.empty() && !env_.corpus_dir[0].empty()) { | |
| WriteToLocalHashedFileInDir(env_.corpus_dir[0], input); | |
| } | |
| } | |
| } | |
| void Centipede::LoadSeedInputs(BlobFileWriter *absl_nonnull corpus_file, | |
| BlobFileWriter *absl_nonnull features_file) { | |
| std::vector<ByteArray> seed_inputs; | |
| const size_t num_seeds_available = | |
| user_callbacks_.GetSeeds(env_.batch_size, seed_inputs); | |
| if (num_seeds_available > env_.batch_size) { | |
| FUZZTEST_LOG(WARNING) << "More seeds available than requested: " | |
| << num_seeds_available << " > " << env_.batch_size; | |
| } | |
| if (seed_inputs.empty()) { | |
| FUZZTEST_QCHECK(!env_.require_seeds) | |
| << "No seeds returned and --require_seeds=true, exiting early."; | |
| FUZZTEST_LOG(WARNING) | |
| << "No seeds returned - will use the default seed of single byte {0}"; | |
| seed_inputs.push_back({0}); | |
| } | |
| RunBatch(InputsToMutantRefs({seed_inputs.begin(), seed_inputs.end()}), | |
| corpus_file, features_file, | |
| /*unconditional_features_file=*/nullptr); | |
| FUZZTEST_LOG(INFO) << "Number of input seeds available: " | |
| << num_seeds_available | |
| << ", number included in corpus: " << corpus_.NumTotal(); | |
| // Forcely add all seed inputs to avoid empty corpus if none of them increased | |
| // coverage and passed the filters. | |
| if (corpus_.NumTotal() == 0) { | |
| for (const auto &seed_input : seed_inputs) | |
| corpus_.Add(seed_input, /*fv=*/{}, /*metadata=*/{}, /*stats=*/{}, fs_, | |
| coverage_frontier_); | |
| } | |
| } | |
| void Centipede::FuzzingLoop() { | |
| FUZZTEST_LOG(INFO) << "Shard: " << env_.my_shard_index << "/" | |
| << env_.total_shards << " " << TemporaryLocalDirPath() | |
| << " " | |
| << "seed: " << env_.seed << "\n\n\n"; | |
| if (env_.load_shards_only) { | |
| UpdateAndMaybeLogStats("begin-load-shard", 0); | |
| } else { | |
| UpdateAndMaybeLogStats("begin-fuzz", 0); | |
| } | |
| if (env_.full_sync) { | |
| LoadAllShardsInRandomOrder(env_, /*rerun_my_shard=*/true); | |
| } else { | |
| LoadShard(env_, env_.my_shard_index, /*rerun=*/true); | |
| } | |
| if (!env_.merge_from.empty()) { | |
| // Merge a shard with the same index from another corpus. | |
| MergeFromOtherCorpus(env_.merge_from, env_.my_shard_index); | |
| } | |
| if (env_.load_shards_only) { | |
| if (env_.persistent_mode) { | |
| user_callbacks_.CleanUpPersistentMode(); | |
| } | |
| UpdateAndMaybeLogStats("end-load-shard", 0); | |
| return; | |
| } | |
| auto corpus_path = wd_.CorpusFilePaths().Shard(env_.my_shard_index); | |
| auto corpus_file = DefaultBlobFileWriterFactory(env_.riegeli); | |
| FUZZTEST_CHECK_OK(corpus_file->Open(corpus_path, "a")); | |
| auto features_path = wd_.FeaturesFilePaths().Shard(env_.my_shard_index); | |
| auto features_file = DefaultBlobFileWriterFactory(env_.riegeli); | |
| FUZZTEST_CHECK_OK(features_file->Open(features_path, "a")); | |
| LoadSeedInputs(corpus_file.get(), features_file.get()); | |
| UpdateAndMaybeLogStats("init-done", 0); | |
| // If we're going to fuzz, dump the initial telemetry files. For a brand-new | |
| // run, these will be functionally empty, e.g. the coverage report will list | |
| // all target functions as not covered (NONE). For a bootstrapped run (the | |
| // workdir already has data), these may or may not coincide with the final | |
| // "latest" report of the previous run, depending on how the runs are | |
| // configured (the same number of shards, for example). | |
| if (env_.num_runs != 0) MaybeGenerateTelemetry("initial", "Before fuzzing"); | |
| // Reset fuzz_start_time_ and num_runs_, so that the pre-init work doesn't | |
| // affect them. | |
| fuzz_start_time_ = absl::Now(); | |
| num_runs_ = 0; | |
| // num_runs / batch_size, rounded up. | |
| size_t number_of_batches = env_.num_runs / env_.batch_size; | |
| if (env_.num_runs % env_.batch_size != 0) ++number_of_batches; | |
| size_t new_runs = 0; | |
| size_t corpus_size_at_last_prune = corpus_.NumActive(); | |
| for (size_t batch_index = 0; batch_index < number_of_batches; batch_index++) { | |
| if (stop_condition_.ShouldStop()) break; | |
| FUZZTEST_CHECK_LT(new_runs, env_.num_runs); | |
| auto remaining_runs = env_.num_runs - new_runs; | |
| auto batch_size = std::min(env_.batch_size, remaining_runs); | |
| std::vector<MutationInputRef> mutation_inputs; | |
| std::vector<size_t> mutate_input_to_corpus_idx; | |
| mutation_inputs.reserve(env_.mutate_batch_size); | |
| mutate_input_to_corpus_idx.reserve(env_.mutate_batch_size); | |
| for (size_t i = 0; i < env_.mutate_batch_size; i++) { | |
| const size_t origin = env_.use_corpus_weights | |
| ? corpus_.WeightedRandom(rng_) | |
| : corpus_.UniformRandom(rng_); | |
| mutate_input_to_corpus_idx.push_back(origin); | |
| const auto& corpus_record = corpus_.Records()[origin]; | |
| mutation_inputs.push_back( | |
| MutationInputRef{corpus_record.data, &corpus_record.metadata}); | |
| } | |
| std::vector<Mutant> mutants = | |
| user_callbacks_.Mutate(mutation_inputs, batch_size); | |
| if (stop_condition_.ShouldStop()) break; | |
| new_runs += mutants.size(); | |
| std::vector<MutantRef> mutant_refs; | |
| mutant_refs.reserve(mutants.size()); | |
| for (auto& mutant : mutants) { | |
| if (mutant.origin == Mutant::kOriginNone) { | |
| mutant_refs.push_back(MutantRef{mutant.data, Mutant::kOriginNone}); | |
| } else { | |
| FUZZTEST_CHECK_LT(mutant.origin, mutate_input_to_corpus_idx.size()); | |
| mutant_refs.push_back( | |
| MutantRef{mutant.data, mutate_input_to_corpus_idx[mutant.origin]}); | |
| } | |
| } | |
| bool gained_new_coverage = | |
| RunBatch(mutant_refs, corpus_file.get(), features_file.get(), nullptr); | |
| if (gained_new_coverage) { | |
| UpdateAndMaybeLogStats("new-feature", 1); | |
| } else if (((batch_index - 1) & batch_index) == 0) { | |
| // Log if batch_index is a power of two. | |
| UpdateAndMaybeLogStats("pulse", 1); | |
| } | |
| // Dump the intermediate telemetry files. | |
| MaybeGenerateTelemetryAfterBatch("latest", batch_index); | |
| if (env_.load_other_shard_frequency != 0 && batch_index != 0 && | |
| (batch_index % env_.load_other_shard_frequency) == 0 && | |
| env_.total_shards > 1) { | |
| size_t rand = rng_() % (env_.total_shards - 1); | |
| size_t other_shard_index = | |
| (env_.my_shard_index + 1 + rand) % env_.total_shards; | |
| FUZZTEST_CHECK_NE(other_shard_index, env_.my_shard_index); | |
| LoadShard(env_, other_shard_index, /*rerun=*/false); | |
| } | |
| // Prune if we added enough new elements since last prune. | |
| if (env_.prune_frequency != 0 && | |
| corpus_.NumActive() > | |
| corpus_size_at_last_prune + env_.prune_frequency) { | |
| if (env_.use_coverage_frontier) coverage_frontier_.Compute(corpus_); | |
| corpus_.Prune(fs_, coverage_frontier_, env_.max_corpus_size, rng_); | |
| corpus_size_at_last_prune = corpus_.NumActive(); | |
| } | |
| } | |
| if (env_.persistent_mode) { | |
| user_callbacks_.CleanUpPersistentMode(); | |
| } | |
| // The tests rely on this stat being logged last. | |
| UpdateAndMaybeLogStats("end-fuzz", 0); | |
| // If we've fuzzed anything, dump the final telemetry files. | |
| if (env_.num_runs != 0) MaybeGenerateTelemetry("final", "After fuzzing"); | |
| } | |
| void Centipede::ReportCrash(std::string_view binary, | |
| absl::Span<const ByteSpan> input_vec, | |
| const BatchResult& batch_result) { | |
| FUZZTEST_CHECK_EQ(input_vec.size(), batch_result.results().size()); | |
| if (batch_result.IsIgnoredFailure()) return; | |
| const size_t suspect_input_idx = std::clamp<size_t>( | |
| batch_result.num_outputs_read(), 0, input_vec.size() - 1); | |
| auto log_execution_failure = [&](std::string_view log_prefix) { | |
| absl::MutexLock lock(GetExecutionLoggingMutex()); | |
| FUZZTEST_LOG(INFO) | |
| << log_prefix << "Batch execution failed:" | |
| << "\nBinary : " << binary | |
| << "\nExit code : " << batch_result.exit_code() | |
| << "\nFailure : " << batch_result.failure_description() | |
| << "\nSignature : " | |
| << AsPrintableString(AsByteSpan(batch_result.failure_signature()), | |
| /*max_len=*/kHashLen) | |
| << "\nNumber of inputs : " << input_vec.size() | |
| << "\nNumber of inputs read: " << batch_result.num_outputs_read() | |
| << (batch_result.IsSetupFailure() | |
| ? "" | |
| : absl::StrCat("\nSuspect input index : ", suspect_input_idx)) | |
| << "\nCrash log :\n\n"; | |
| for (const auto &log_line : | |
| absl::StrSplit(absl::StripAsciiWhitespace(batch_result.log()), '\n')) { | |
| FUZZTEST_LOG(INFO).NoPrefix() << "CRASH LOG: " << log_line; | |
| } | |
| FUZZTEST_LOG(INFO).NoPrefix() << "\n"; | |
| }; | |
| if (batch_result.IsSkippedTest()) { | |
| log_execution_failure("Skipped Test: "); | |
| FUZZTEST_LOG(INFO) << "Requesting early stop due to skipped test."; | |
| stop_condition_.RequestStop(EXIT_SUCCESS, "The test was skipped"); | |
| return; | |
| } | |
| if (batch_result.IsSetupFailure()) { | |
| log_execution_failure("Test Setup Failure: "); | |
| FUZZTEST_LOG(INFO) | |
| << "Requesting early stop due to setup failure in the test."; | |
| stop_condition_.RequestStop(EXIT_FAILURE, "Setup failed in the test"); | |
| return; | |
| } | |
| // Skip reporting only if RequestStop is called - still reporting if time | |
| // limit is reached. | |
| if (stop_condition_.StopRequested()) return; | |
| if (++num_crashes_ > env_.max_num_crash_reports) return; | |
| const std::string log_prefix = | |
| absl::StrCat("ReportCrash[", num_crashes_, "]: "); | |
| log_execution_failure(log_prefix); | |
| FUZZTEST_LOG_IF(INFO, num_crashes_ == env_.max_num_crash_reports) | |
| << log_prefix | |
| << "Reached --max_num_crash_reports: further reports will be suppressed"; | |
| if (batch_result.failure_description() == kExecutionFailurePerBatchTimeout) { | |
| FUZZTEST_LOG(INFO) | |
| << log_prefix | |
| << "Failure applies to entire batch: not executing inputs " | |
| "one-by-one, trying to find the reproducer"; | |
| return; | |
| } | |
| if (input_vec.size() == 1 && !env_.persistent_mode && | |
| batch_result.IsInputFailure()) { | |
| FUZZTEST_LOG(INFO) | |
| << log_prefix | |
| << "Single-input batch in non-persistent mode; skipping re-execution " | |
| "and saving the reproducer directly."; | |
| SaveReproducer(wd_, log_prefix, /*input_idx=*/0, input_vec[0], | |
| batch_result); | |
| return; | |
| } | |
| // Determine the optimal order of the inputs to try to maximize the chances of | |
| // finding the reproducer fast. | |
| std::vector<size_t> input_idxs_to_try; | |
| // Prioritize the presumed crasher by inserting it in front of everything | |
| // else. | |
| input_idxs_to_try.push_back(suspect_input_idx); | |
| if (!env_.batch_triage_suspect_only) { | |
| // TODO(b/274705740): When the bug is fixed, set `input_idxs_to_try`'s size | |
| // to `suspect_input_idx + 1`. | |
| input_idxs_to_try.resize(input_vec.size() + 1); | |
| // Keep the suspect at the old location, too, in case the target was | |
| // primed for a crash by the sequence of inputs that preceded the crasher. | |
| std::iota(input_idxs_to_try.begin() + 1, input_idxs_to_try.end(), 0); | |
| } else { | |
| FUZZTEST_LOG(INFO) | |
| << log_prefix | |
| << "Skip finding the reproducer from the inputs other than the suspect"; | |
| } | |
| // Try inputs one-by-one in the determined order. | |
| FUZZTEST_LOG(INFO) | |
| << log_prefix | |
| << "Executing inputs one-by-one, trying to find the reproducer"; | |
| const size_t max_attempts = std::max<size_t>(1, env_.replay_crash_attempts); | |
| for (auto input_idx : input_idxs_to_try) { | |
| if (stop_condition_.ShouldStop()) break; | |
| const auto one_input = input_vec[input_idx]; | |
| for (size_t attempt = 0; attempt < max_attempts; ++attempt) { | |
| if (stop_condition_.ShouldStop()) break; | |
| BatchResult one_input_batch_result; | |
| if (!user_callbacks_.Execute(binary, {one_input}, | |
| one_input_batch_result) && | |
| one_input_batch_result.IsInputFailure() && | |
| one_input_batch_result.failure_signature() == | |
| batch_result.failure_signature() && | |
| !stop_condition_.ShouldStop()) { | |
| SaveReproducer(wd_, log_prefix, input_idx, one_input, | |
| one_input_batch_result); | |
| return; | |
| } | |
| } | |
| } | |
| if (stop_condition_.ShouldStop()) { | |
| FUZZTEST_LOG(INFO) | |
| << log_prefix | |
| << "Stop condition is on - skipping triaging the reproducer"; | |
| return; | |
| } | |
| FUZZTEST_LOG(INFO) << log_prefix | |
| << "Crash was not observed when running inputs one-by-one"; | |
| // There will be cases when several inputs collectively cause a crash, but no | |
| // single input does. Handle this by writing out the inputs from the batch | |
| // between 0 and `suspect_input_idx` (inclusive) as individual files under the | |
| // <--workdir>/crash/crashing_batch-<HASH_OF_SUSPECT_INPUT> directory. | |
| // TODO(bookholt): Check for repro by re-running the whole batch. | |
| // TODO(ussuri): Consolidate the crash reproduction logic here and above. | |
| // TODO(ussuri): This can create a lot of tiny files. Write to a single | |
| // shard-like corpus file instead. | |
| const auto &suspect_input = input_vec[suspect_input_idx]; | |
| auto suspect_hash = Hash(suspect_input); | |
| auto crash_dir = wd_.CrashReproducerDirPaths().MyShard(); | |
| FUZZTEST_CHECK_OK(RemoteMkdir(crash_dir)); | |
| std::string crashing_batch_name = | |
| absl::StrCat("crashing_batch-", suspect_hash); | |
| std::string save_dir = std::filesystem::path(crash_dir) / crashing_batch_name; | |
| FUZZTEST_CHECK_OK(RemoteMkdir(save_dir)); | |
| FUZZTEST_LOG(INFO) << log_prefix | |
| << "Saving used inputs from batch to: " << save_dir; | |
| for (int i = 0; i <= suspect_input_idx; ++i) { | |
| const auto &one_input = input_vec[i]; | |
| auto hash = Hash(one_input); | |
| std::string file_path = std::filesystem::path(save_dir).append( | |
| absl::StrFormat("input-%010d-%s", i, hash)); | |
| FUZZTEST_CHECK_OK(RemoteFileSetContents(file_path, one_input)); | |
| } | |
| auto crash_metadata_dir = wd_.CrashMetadataDirPaths().MyShard(); | |
| FUZZTEST_CHECK_OK(RemoteMkdir(crash_metadata_dir)); | |
| std::string crash_metadata_file_path = | |
| std::filesystem::path(crash_metadata_dir) / crashing_batch_name; | |
| FUZZTEST_LOG(INFO) << log_prefix << "Saving crash metadata to: " | |
| << crash_metadata_file_path; | |
| FUZZTEST_CHECK_OK(RemoteFileSetContents(crash_metadata_file_path, | |
| batch_result.failure_description())); | |
| } | |
| } // namespace fuzztest::internal |