// 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 #include #include #include #include #include #include // NOLINT #include #include #include #include #include #include #include #include #include #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/log/check.h" #include "absl/log/log.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 "./centipede/binary_info.h" #include "./centipede/centipede_callbacks.h" #include "./centipede/command.h" #include "./centipede/control_flow.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_input.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 { Centipede::Centipede(const Environment &env, CentipedeCallbacks &user_callbacks, const BinaryInfo &binary_info, CoverageLogger &coverage_logger, std::atomic &stats) : env_(env), 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()), 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 = "/dev/null"; cmd_options.stderr_file = "/dev/null"; 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") { 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 sharded_corpus_files; 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> sharded_paths(env.total_shards); std::vector paths; size_t total_paths = 0; const std::vector listed_paths = ValueOrDie(RemoteListFiles(dir, /*recursively=*/true)); for (const std::string &path : listed_paths) { size_t filename_hash = std::hash{}(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()) { 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 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); 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; CHECK_OK(RemoteFileGetContents(path, input)); if (input.empty() || existing_hashes.contains(Hash(input))) { ++inputs_ignored; continue; } CHECK_OK(appender->Write(ByteArray{input.begin(), input.end()})); ++inputs_added; } LOG(INFO) << VV(shard) << VV(inputs_added) << VV(inputs_ignored) << VV(num_shard_bytes) << VV(shard_data.size()); } CHECK_EQ(total_paths, inputs_added + inputs_ignored); } absl::Status Centipede::CrashesToFiles(const Environment &env, std::string_view dir) { std::vector 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 crash_ids; for (const auto &reproducer_dir : reproducer_dirs) { ASSIGN_OR_RETURN_IF_NOT_OK( std::vector 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()); 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(absl::ToUnixMicros(absl::Now())), }, ExecStats{ /*fuzz_time_sec=*/static_cast(std::ceil(fuzz_time_secs)), /*num_executions*/ num_runs_, /*num_target_crashes*/ static_cast(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( std::lround(rusage_timing.cpu_hyper_cores * 1000)), /*engine_rusage_cpu_percent=*/ static_cast( std::lround(rusage_timing.cpu_utilization * 100)), /*engine_rusage_rss_mb=*/ static_cast(rusage_memory.mem_rss >> 20), /*engine_rusage_vsize_mb=*/ static_cast(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 << " mb: " << (rusage_memory.mem_rss >> 20); 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; LOG(INFO) << description; } } bool Centipede::InputPassesFilter(const ByteArray &input) { if (env_.input_filter.empty()) return true; WriteToLocalFile(input_filter_path_, input); bool result = input_filter_cmd_.Execute() == EXIT_SUCCESS; std::filesystem::remove(input_filter_path_); return result; } bool Centipede::ExecuteAndReportCrash(std::string_view binary, const std::vector &input_vec, BatchResult &batch_result) { bool success = user_callbacks_.Execute(binary, input_vec, batch_result); if (!success) ReportCrash(binary, input_vec, batch_result); return success || batch_result.IsIgnoredFailure(); } // *** 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( const std::vector &input_vec, BlobFileWriter *absl_nullable corpus_file, BlobFileWriter *absl_nullable features_file, BlobFileWriter *absl_nullable unconditional_features_file) { BatchResult batch_result; bool success = ExecuteAndReportCrash(env_.binary, input_vec, batch_result); CHECK_EQ(input_vec.size(), batch_result.results().size()); for (const auto &extra_binary : env_.extra_binaries) { if (ShouldStop()) break; BatchResult extra_batch_result; success = ExecuteAndReportCrash(extra_binary, input_vec, extra_batch_result) && success; } if (EarlyStopRequested()) return false; if (!success && env_.exit_on_crash) { LOG(INFO) << "--exit_on_crash is enabled; exiting soon"; RequestEarlyStop(EXIT_FAILURE); return false; } CHECK_EQ(batch_result.results().size(), input_vec.size()); num_runs_ += input_vec.size(); bool batch_gained_new_coverage = false; for (size_t i = 0; i < input_vec.size(); i++) { if (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; if (unconditional_features_file != nullptr) { CHECK_OK(unconditional_features_file->Write( PackFeaturesAndHash(input_vec[i], fv))); } if (input_gained_new_coverage) { // TODO(kcc): [impl] add stats for filtered-out inputs. if (!InputPassesFilter(input_vec[i])) continue; fs_.IncrementFrequencies(fv); LogFeaturesAsSymbols(fv); batch_gained_new_coverage = true; CHECK_GT(fv.size(), 0UL); if (function_filter_passed) { corpus_.Add(input_vec[i], fv, batch_result.results()[i].metadata(), fs_, coverage_frontier_); } if (corpus_file != nullptr) { CHECK_OK(corpus_file->Write(input_vec[i])); } if (!env_.corpus_dir.empty() && !env_.corpus_dir[0].empty()) { WriteToLocalHashedFileInDir(env_.corpus_dir[0], input_vec[i]); } if (features_file != nullptr) { CHECK_OK(features_file->Write(PackFeaturesAndHash(input_vec[i], fv))); } } } 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) { 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 inputs_to_rerun; auto input_features_callback = [&](ByteArray input, FeatureVec input_features) { if (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) { VLOG(10) << "Adding input " << Hash(input) << "; new features: " << num_new_features; fs_.IncrementFrequencies(input_features); // TODO(kcc): cmp_args are currently not saved to disk and not reloaded. corpus_.Add(input, input_features, {}, fs_, coverage_frontier_); ++num_added_inputs; } else { 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); } 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 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); LOG_IF(INFO, (++num_shards_loaded % 100) == 0) << VV(num_shards_loaded); } } void Centipede::Rerun(std::vector &to_rerun) { if (to_rerun.empty()) return; auto features_file_path = wd_.FeaturesFilePaths().Shard(env_.my_shard_index); auto features_file = DefaultBlobFileWriterFactory(env_.riegeli); CHECK_OK(features_file->Open(features_file_path, "a")); LOG(INFO) << to_rerun.size() << " inputs to rerun"; // Re-run all inputs for which we don't know their features. // Run in batches of at most env_.batch_size inputs each. while (!to_rerun.empty()) { if (ShouldStop()) break; size_t batch_size = std::min(to_rerun.size(), env_.batch_size); std::vector batch(to_rerun.end() - batch_size, to_rerun.end()); to_rerun.resize(to_rerun.size() - batch_size); if (RunBatch(batch, nullptr, nullptr, features_file.get())) { UpdateAndMaybeLogStats("rerun-old", 1); } } } void Centipede::GenerateCoverageReport(std::string_view filename_annotation, std::string_view description) { if (pc_table_.empty()) return; auto coverage_path = wd_.CoverageReportPath(filename_annotation); 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); 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); LOG(INFO) << "Generate source based coverage report [" << description << "]; " << VV(report_path); CHECK_OK(RemoteMkdir(report_path)); std::vector raw_profiles = wd_.EnumerateRawCoverageProfiles(); if (raw_profiles.empty()) { LOG(ERROR) << "No raw profiles found for coverage report"; return; } std::string indexed_profile_path = wd_.SourceBasedCoverageIndexedProfilePath(); std::vector 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) { 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) { 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")} { CHECK(file_ != nullptr) << VV(path); CHECK_OK(RemoteFileSetWriteBufferSize(file_, 10UL * 1024 * 1024)); } ~ReportDumper() override { CHECK_OK(RemoteFileClose(file_)); } ReportDumper &operator<<(std::string_view fragment) override { 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}); VLOG(1) << "Rusage @ " << description << ": " << snapshot.ShortMetricsStr(); auto path = wd_.RUsageReportPath(filename_annotation); 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) { 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(); 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); CHECK_OK( appender->Open(wd_.CorpusFilePaths().Shard(env_.my_shard_index), "a")); for (size_t idx = initial_corpus_size; idx < new_corpus_size; ++idx) { CHECK_OK(appender->Write(corpus_.Get(idx))); } 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(); 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. CHECK_OK(appender->Open(distill_to_path, "w")); for (size_t i = 0; i < corpus_.NumActive(); ++i) { const ByteArray &input = corpus_.Get(i); 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 seed_inputs; const size_t num_seeds_available = user_callbacks_.GetSeeds(env_.batch_size, seed_inputs); if (num_seeds_available > env_.batch_size) { LOG(WARNING) << "More seeds available than requested: " << num_seeds_available << " > " << env_.batch_size; } if (seed_inputs.empty()) { QCHECK(!env_.require_seeds) << "No seeds returned and --require_seeds=true, exiting early."; LOG(WARNING) << "No seeds returned - will use the default seed of single byte {0}"; seed_inputs.push_back({0}); } RunBatch(seed_inputs, corpus_file, features_file, /*unconditional_features_file=*/nullptr); 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, {}, {}, fs_, coverage_frontier_); } } void Centipede::FuzzingLoop() { LOG(INFO) << "Shard: " << env_.my_shard_index << "/" << env_.total_shards << " " << TemporaryLocalDirPath() << " " << "seed: " << env_.seed << "\n\n\n"; 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) return; auto corpus_path = wd_.CorpusFilePaths().Shard(env_.my_shard_index); auto corpus_file = DefaultBlobFileWriterFactory(env_.riegeli); CHECK_OK(corpus_file->Open(corpus_path, "a")); auto features_path = wd_.FeaturesFilePaths().Shard(env_.my_shard_index); auto features_file = DefaultBlobFileWriterFactory(env_.riegeli); 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 (ShouldStop()) break; 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 mutation_inputs; mutation_inputs.reserve(env_.mutate_batch_size); for (size_t i = 0; i < env_.mutate_batch_size; i++) { const auto &corpus_record = env_.use_corpus_weights ? corpus_.WeightedRandom(rng_()) : corpus_.UniformRandom(rng_()); mutation_inputs.push_back( MutationInputRef{corpus_record.data, &corpus_record.metadata}); } const std::vector mutants = user_callbacks_.Mutate(mutation_inputs, batch_size); if (ShouldStop()) break; bool gained_new_coverage = RunBatch(mutants, corpus_file.get(), features_file.get(), nullptr); new_runs += mutants.size(); 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; 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(); } } // 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, const std::vector &input_vec, const BatchResult &batch_result) { CHECK_EQ(input_vec.size(), batch_result.results().size()); const size_t suspect_input_idx = std::clamp( batch_result.num_outputs_read(), 0, input_vec.size() - 1); auto log_execution_failure = [&](std::string_view log_prefix) { 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=*/32) << "\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')) { LOG(INFO).NoPrefix() << "CRASH LOG: " << log_line; } LOG(INFO).NoPrefix() << "\n"; }; if (batch_result.IsIgnoredFailure()) { LOG(INFO) << "Skip further processing of " << batch_result.failure_description(); return; } if (batch_result.IsSkippedTest()) { log_execution_failure("Skipped Test: "); LOG(INFO) << "Requesting early stop due to skipped test."; RequestEarlyStop(EXIT_SUCCESS); return; } if (batch_result.IsSetupFailure()) { log_execution_failure("Test Setup Failure: "); LOG(INFO) << "Requesting early stop due to setup failure in the test."; RequestEarlyStop(EXIT_FAILURE); return; } // Skip reporting only if RequestEarlyStop is called - still reporting if time // runs out. if (EarlyStopRequested()) 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); 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) { LOG(INFO) << log_prefix << "Failure applies to entire batch: not executing inputs " "one-by-one, trying to find the reproducer"; return; } // Determine the optimal order of the inputs to try to maximize the chances of // finding the reproducer fast. std::vector 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 { LOG(INFO) << log_prefix << "Skip finding the reproducer from the inputs other than the suspect"; } // Try inputs one-by-one in the determined order. LOG(INFO) << log_prefix << "Executing inputs one-by-one, trying to find the reproducer"; for (auto input_idx : input_idxs_to_try) { if (ShouldStop()) return; const auto &one_input = input_vec[input_idx]; BatchResult one_input_batch_result; if (!user_callbacks_.Execute(binary, {one_input}, one_input_batch_result)) { auto hash = Hash(one_input); auto crash_dir = wd_.CrashReproducerDirPaths().MyShard(); CHECK_OK(RemoteMkdir(crash_dir)); std::string input_file_path = std::filesystem::path(crash_dir) / hash; auto crash_metadata_dir = wd_.CrashMetadataDirPaths().MyShard(); CHECK_OK(RemoteMkdir(crash_metadata_dir)); std::string crash_metadata_path_prefix = std::filesystem::path(crash_metadata_dir) / hash; LOG(INFO) << log_prefix << "Detected crash-reproducing input:" << "\nInput index : " << input_idx << "\nInput bytes : " << AsPrintableString(one_input, /*max_len=*/32) << "\nExit code : " << one_input_batch_result.exit_code() << "\nFailure : " << one_input_batch_result.failure_description() << "\nSignature : " << AsPrintableString( AsByteSpan(one_input_batch_result.failure_signature()), /*max_len=*/32) << "\nSaving input to: " << input_file_path << "\nSaving crash" // << "\nmetadata to : " << crash_metadata_path_prefix << ".*"; CHECK_OK(RemoteFileSetContents(input_file_path, one_input)); CHECK_OK(RemoteFileSetContents( absl::StrCat(crash_metadata_path_prefix, ".desc"), one_input_batch_result.failure_description())); CHECK_OK(RemoteFileSetContents( absl::StrCat(crash_metadata_path_prefix, ".sig"), one_input_batch_result.failure_signature())); return; } } 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- 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(); 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; CHECK_OK(RemoteMkdir(save_dir)); 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)); CHECK_OK(RemoteFileSetContents(file_path, one_input)); } auto crash_metadata_dir = wd_.CrashMetadataDirPaths().MyShard(); CHECK_OK(RemoteMkdir(crash_metadata_dir)); std::string crash_metadata_file_path = std::filesystem::path(crash_metadata_dir) / crashing_batch_name; LOG(INFO) << log_prefix << "Saving crash metadata to: " << crash_metadata_file_path; CHECK_OK(RemoteFileSetContents(crash_metadata_file_path, batch_result.failure_description())); } } // namespace fuzztest::internal