| // 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. | |
| #include "./centipede/centipede_interface.h" | |
| #include <unistd.h> | |
| #include <algorithm> | |
| #include <atomic> | |
| #include <cstdint> | |
| #include <cstdlib> | |
| #include <cstring> | |
| #include <filesystem> // NOLINT | |
| #include <iostream> | |
| #include <memory> | |
| #include <string> | |
| #include <string_view> | |
| #include <utility> | |
| #include <vector> | |
| #include "absl/cleanup/cleanup.h" | |
| #include "absl/container/flat_hash_map.h" | |
| #include "absl/status/status.h" | |
| #include "absl/status/statusor.h" | |
| #include "absl/strings/ascii.h" | |
| #include "absl/strings/escaping.h" | |
| #include "absl/strings/str_cat.h" | |
| #include "absl/strings/str_format.h" | |
| #include "absl/strings/str_join.h" | |
| #include "absl/strings/str_replace.h" | |
| #include "absl/time/clock.h" | |
| #include "absl/time/time.h" | |
| #include "absl/types/span.h" | |
| #include "./centipede/analyze_corpora.h" | |
| #include "./centipede/binary_info.h" | |
| #include "./centipede/centipede.h" | |
| #include "./centipede/centipede_callbacks.h" | |
| #include "./centipede/command.h" | |
| #include "./centipede/coverage.h" | |
| #include "./centipede/crash_deduplication.h" | |
| #include "./centipede/crash_summary.h" | |
| #include "./centipede/distill.h" | |
| #include "./centipede/environment.h" | |
| #include "./centipede/minimize_crash.h" | |
| #include "./centipede/pc_info.h" | |
| #include "./centipede/periodic_action.h" | |
| #include "./centipede/runner_result.h" | |
| #include "./centipede/seed_corpus_maker_lib.h" | |
| #include "./centipede/stats.h" | |
| #include "./centipede/stop.h" | |
| #include "./centipede/thread_pool.h" | |
| #include "./centipede/util.h" | |
| #include "./centipede/workdir.h" | |
| #include "./common/bazel.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" | |
| #include "./fuzztest/internal/configuration.h" | |
| namespace fuzztest::internal { | |
| namespace { | |
| constexpr absl::Duration kDefaultRegressionTtl = absl::Hours(24 * 7); | |
| // Runs env.for_each_blob on every blob extracted from env.args. | |
| // Returns EXIT_SUCCESS on success, EXIT_FAILURE otherwise. | |
| void ForEachBlob(const Environment& env, StopCondition& stop_condition) { | |
| auto tmpdir = TemporaryLocalDirPath(); | |
| CreateLocalDirRemovedAtExit(tmpdir); | |
| std::string tmpfile = std::filesystem::path(tmpdir).append("t"); | |
| for (const auto& arg : env.args) { | |
| FUZZTEST_LOG(INFO) << "Running '" << env.for_each_blob << "' on " << arg; | |
| auto blob_reader = DefaultBlobFileReaderFactory(); | |
| absl::Status open_status = blob_reader->Open(arg); | |
| if (!open_status.ok()) { | |
| const std::string stop_reason = | |
| absl::StrCat("Failed to open ", arg, ": ", open_status); | |
| FUZZTEST_LOG(WARNING) << stop_reason; | |
| stop_condition.RequestStop(EXIT_FAILURE, stop_reason); | |
| return; | |
| } | |
| ByteSpan blob; | |
| while (blob_reader->Read(blob) == absl::OkStatus()) { | |
| WriteToLocalFile(tmpfile, blob); | |
| std::string command_line = absl::StrReplaceAll( | |
| env.for_each_blob, {{"%P", tmpfile}, {"%H", Hash(blob)}}); | |
| Command cmd(command_line); | |
| // TODO(kcc): [as-needed] this creates one process per blob. | |
| // If this flag gets active use, we may want to define special cases, | |
| // e.g. if for_each_blob=="cp %P /some/where" we can do it in-process. | |
| cmd.Execute(); | |
| if (stop_condition.ShouldStop()) return; | |
| } | |
| } | |
| } | |
| void MinimizeCrash(const Environment& env, | |
| CentipedeCallbacksFactory& callbacks_factory, | |
| StopCondition& stop_condition) { | |
| ByteArray crashy_input; | |
| ReadFromLocalFile(env.minimize_crash_file_path, crashy_input); | |
| BatchResult batch_result; | |
| { | |
| ScopedCentipedeCallbacks scoped_callbacks(callbacks_factory, env, | |
| stop_condition); | |
| CreateLocalDirRemovedAtExit(TemporaryLocalDirPath()); | |
| if (scoped_callbacks.callbacks()->Execute(env.binary, {crashy_input}, | |
| batch_result)) { | |
| FUZZTEST_LOG(ERROR) << "The original crashy input did not crash; exiting"; | |
| stop_condition.RequestStop(EXIT_FAILURE, | |
| "The original crashy input did not crash"); | |
| return; | |
| } | |
| } | |
| const auto result = | |
| MinimizeCrash(crashy_input, env, callbacks_factory, | |
| batch_result.failure_signature(), stop_condition); | |
| if (!result.has_value()) { | |
| stop_condition.RequestStop(EXIT_FAILURE, "No smaller crash found"); | |
| return; | |
| } | |
| const auto output_dir = | |
| std::filesystem::path{WorkDir{env}.CrashReproducerDirPaths().MyShard()}; | |
| const auto mkdir_status = RemoteMkdir(output_dir.string()); | |
| if (!mkdir_status.ok()) { | |
| const auto error_msg = | |
| absl::StrCat("Failed to create the minimized crasher dir ", | |
| output_dir.string(), ": ", mkdir_status); | |
| FUZZTEST_LOG(ERROR) << error_msg; | |
| stop_condition.RequestStop(EXIT_FAILURE, error_msg); | |
| return; | |
| } | |
| const auto result_input_hash = Hash(result->input); | |
| const auto output_path = (output_dir / result_input_hash).string(); | |
| const auto write_status = RemoteFileSetContents(output_path, result->input); | |
| if (!write_status.ok()) { | |
| const auto error_msg = | |
| absl::StrCat("Failed to write the minimized crasher to ", output_path, | |
| ": ", write_status); | |
| FUZZTEST_LOG(ERROR) << error_msg; | |
| stop_condition.RequestStop(EXIT_FAILURE, error_msg); | |
| return; | |
| } | |
| } | |
| // Loads corpora from work dirs provided in `env.args`, if there are two args | |
| // provided, analyzes differences. If there is one arg provided, reports the | |
| // function coverage. Returns EXIT_SUCCESS on success, EXIT_FAILURE otherwise. | |
| int Analyze(const Environment& env) { | |
| FUZZTEST_LOG(INFO) << "Analyze " << absl::StrJoin(env.args, ","); | |
| FUZZTEST_CHECK(!env.binary.empty()) << "--binary must be used"; | |
| if (env.args.size() == 1) { | |
| const CoverageResults coverage_results = | |
| GetCoverage(env.binary_name, env.binary_hash, env.args[0]); | |
| WorkDir workdir{env}; | |
| const std::string coverage_report_path = | |
| workdir.CoverageReportPath(/*annotation=*/""); | |
| DumpCoverageReport(coverage_results, coverage_report_path); | |
| } else if (env.args.size() == 2) { | |
| AnalyzeCorporaToLog(env.binary_name, env.binary_hash, env.args[0], | |
| env.args[1]); | |
| } else { | |
| FUZZTEST_LOG(FATAL) | |
| << "for now, --analyze supports only 1 or 2 work dirs; got " | |
| << env.args.size(); | |
| } | |
| return EXIT_SUCCESS; | |
| } | |
| void SavePCTableToFile(const PCTable& pc_table, std::string_view file_path) { | |
| WriteToLocalFile(file_path, AsByteSpan(pc_table)); | |
| } | |
| BinaryInfo PopulateBinaryInfoAndSavePCsIfNecessary( | |
| const Environment& env, CentipedeCallbacksFactory& callbacks_factory, | |
| std::string& pcs_file_path, StopCondition& stop_condition) { | |
| BinaryInfo binary_info; | |
| // Some fuzz targets have coverage not based on instrumenting binaries. | |
| // For those target, we should not populate binary info. | |
| if (env.populate_binary_info) { | |
| ScopedCentipedeCallbacks scoped_callbacks(callbacks_factory, env, | |
| stop_condition); | |
| scoped_callbacks.callbacks()->PopulateBinaryInfo(binary_info); | |
| if (stop_condition.ShouldStop()) { | |
| return binary_info; | |
| } | |
| } | |
| if (env.save_binary_info) { | |
| const std::string binary_info_dir = WorkDir{env}.BinaryInfoDirPath(); | |
| FUZZTEST_CHECK_OK(RemoteMkdir(binary_info_dir)); | |
| FUZZTEST_LOG(INFO) << "Serializing binary info to: " << binary_info_dir; | |
| binary_info.Write(binary_info_dir); | |
| } | |
| if (binary_info.uses_legacy_trace_pc_instrumentation) { | |
| pcs_file_path = std::filesystem::path(TemporaryLocalDirPath()) / "pcs"; | |
| SavePCTableToFile(binary_info.pc_table, pcs_file_path); | |
| } | |
| if (env.use_pcpair_features) { | |
| FUZZTEST_CHECK(!binary_info.pc_table.empty()) | |
| << "--use_pcpair_features requires non-empty pc_table"; | |
| } | |
| return binary_info; | |
| } | |
| std::vector<Environment> CreateEnvironmentsForThreads( | |
| const Environment& origin_env, std::string_view pcs_file_path) { | |
| std::vector<Environment> envs(origin_env.num_threads, origin_env); | |
| size_t thread_idx = 0; | |
| for (auto& env : envs) { | |
| env.my_shard_index += thread_idx++; | |
| env.UpdateForExperiment(); | |
| env.pcs_file_path = pcs_file_path; | |
| } | |
| return envs; | |
| } | |
| void Fuzz(const Environment& env, const BinaryInfo& binary_info, | |
| std::string_view pcs_file_path, | |
| CentipedeCallbacksFactory& callbacks_factory, | |
| StopCondition& stop_condition) { | |
| CoverageLogger coverage_logger(binary_info.pc_table, binary_info.symbols); | |
| std::vector<Environment> envs = | |
| CreateEnvironmentsForThreads(env, pcs_file_path); | |
| std::vector<std::atomic<Stats>> stats_vec(env.num_threads); | |
| // Start periodic stats dumping and, optionally, logging. | |
| std::vector<PeriodicAction> stats_reporters; | |
| stats_reporters.emplace_back( | |
| [csv_appender = StatsCsvFileAppender{stats_vec, envs}]() mutable { | |
| csv_appender.ReportCurrStats(); | |
| }, | |
| PeriodicAction::Options{ | |
| /*sleep_before_each=*/ | |
| [](size_t iteration) { | |
| return absl::Minutes(std::clamp(iteration, 0UL, 10UL)); | |
| }, | |
| }); | |
| if (!envs.front().experiment.empty() || FUZZTEST_VLOG_IS_ON(1)) { | |
| stats_reporters.emplace_back( | |
| [logger = StatsLogger{stats_vec, envs}]() mutable { | |
| logger.ReportCurrStats(); | |
| }, | |
| PeriodicAction::Options{ | |
| /*sleep_before_each=*/ | |
| [](size_t iteration) { | |
| return absl::Seconds(std::clamp(iteration, 5UL, 600UL)); | |
| }, | |
| }); | |
| } | |
| auto fuzzing_worker = [&env, &callbacks_factory, &binary_info, | |
| &coverage_logger, &stop_condition]( | |
| Environment& my_env, std::atomic<Stats>& stats, | |
| bool create_tmpdir) { | |
| if (create_tmpdir) CreateLocalDirRemovedAtExit(TemporaryLocalDirPath()); | |
| // Uses TID, call in this thread. | |
| my_env.seed = GetRandomSeed(env.seed); | |
| if (env.dry_run) return; | |
| ScopedCentipedeCallbacks scoped_callbacks(callbacks_factory, my_env, | |
| stop_condition); | |
| Centipede centipede(my_env, *scoped_callbacks.callbacks(), binary_info, | |
| coverage_logger, stats, stop_condition); | |
| centipede.FuzzingLoop(); | |
| }; | |
| if (env.num_threads == 1) { | |
| // When fuzzing with one thread, run fuzzing loop in the current | |
| // thread. This is because FuzzTest/Centipede's single-process | |
| // fuzzing requires the test body, which is invoked by the fuzzing | |
| // loop, to run in the main thread. | |
| // | |
| // Here, the fuzzing worker should not re-create the tmpdir since the path | |
| // is thread-local and it has been created in the current function. | |
| fuzzing_worker(envs[0], stats_vec[0], /*create_tmpdir=*/false); | |
| } else { | |
| ThreadPool fuzzing_worker_threads{static_cast<int>(env.num_threads)}; | |
| for (size_t thread_idx = 0; thread_idx < env.num_threads; thread_idx++) { | |
| Environment& my_env = envs[thread_idx]; | |
| std::atomic<Stats>& my_stats = stats_vec[thread_idx]; | |
| fuzzing_worker_threads.Schedule([&fuzzing_worker, &my_env, &my_stats]() { | |
| fuzzing_worker(my_env, my_stats, /*create_tmpdir=*/true); | |
| }); | |
| } // All `fuzzing_worker_threads` join here. | |
| } | |
| for (auto& reporter : stats_reporters) { | |
| // Nudge one final update and stop the reporting thread. | |
| reporter.Nudge(); | |
| reporter.Stop(); | |
| } | |
| if (!env.knobs_file.empty()) PrintRewardValues(stats_vec, std::cerr); | |
| } | |
| TestShard SetUpTestSharding() { | |
| TestShard test_shard = GetBazelTestShard(); | |
| // Update the shard status file to indicate that we support test sharding. | |
| // It suffices to update the file's modification time, but we clear the | |
| // contents for simplicity. This is also what the GoogleTest framework does. | |
| if (const char* test_shard_status_file = | |
| std::getenv("TEST_SHARD_STATUS_FILE"); | |
| test_shard_status_file != nullptr) { | |
| ClearLocalFileContents(test_shard_status_file); | |
| } | |
| return test_shard; | |
| } | |
| // Seeds the corpus files in `env.workdir` with the inputs in `regression_dir` | |
| // (always used) and the previously distilled corpus files from `coverage_dir` | |
| // (used if non-empty). | |
| SeedCorpusConfig GetSeedCorpusConfig(const Environment& env, | |
| std::string_view regression_dir, | |
| std::string_view coverage_dir) { | |
| const WorkDir workdir{env}; | |
| SeedCorpusSource regression; | |
| regression.dir_glob = std::string(regression_dir); | |
| regression.num_recent_dirs = 1; | |
| regression.individual_input_rel_glob = "*"; | |
| regression.sampled_fraction_or_count = 1.0f; | |
| std::vector<SeedCorpusSource> sources = {std::move(regression)}; | |
| if (!coverage_dir.empty()) { | |
| SeedCorpusSource coverage; | |
| coverage.dir_glob = std::string(coverage_dir); | |
| coverage.num_recent_dirs = 1; | |
| // We're using the previously distilled corpus files as seeds. | |
| coverage.shard_rel_glob = | |
| std::filesystem::path{ | |
| workdir.DistilledCorpusFilePaths().AllShardsGlob()} | |
| .filename(); | |
| coverage.individual_input_rel_glob = "*"; | |
| coverage.sampled_fraction_or_count = 1.0f; | |
| sources.push_back(std::move(coverage)); | |
| } | |
| SeedCorpusDestination destination; | |
| destination.dir_path = env.workdir; | |
| // We're seeding the current corpus files. | |
| destination.shard_rel_glob = | |
| std::filesystem::path{workdir.CorpusFilePaths().AllShardsGlob()} | |
| .filename(); | |
| destination.shard_index_digits = WorkDir::kDigitsInShardIndex; | |
| destination.num_shards = static_cast<uint32_t>(env.num_threads); | |
| return { | |
| std::move(sources), | |
| std::move(destination), | |
| }; | |
| } | |
| absl::Duration ReadFuzzingTime(std::string_view fuzzing_time_file) { | |
| std::string fuzzing_time_str; | |
| FUZZTEST_CHECK_OK(RemoteFileGetContents(fuzzing_time_file, fuzzing_time_str)); | |
| absl::Duration fuzzing_time; | |
| if (!absl::ParseDuration(absl::StripAsciiWhitespace(fuzzing_time_str), | |
| &fuzzing_time)) { | |
| FUZZTEST_LOG(WARNING) | |
| << "Failed to parse fuzzing time of a resuming fuzz test: '" | |
| << fuzzing_time_str << "'. Assuming no fuzzing time so far."; | |
| return absl::ZeroDuration(); | |
| } | |
| return fuzzing_time; | |
| } | |
| PeriodicAction RecordFuzzingTime(std::string_view fuzzing_time_file, | |
| absl::Time start_time) { | |
| return {[=] { | |
| absl::Status status = RemoteFileSetContents( | |
| fuzzing_time_file, | |
| absl::FormatDuration(absl::Now() - start_time)); | |
| FUZZTEST_LOG_IF(WARNING, !status.ok()) | |
| << "Failed to write fuzzing time: " << status; | |
| }, | |
| PeriodicAction::ZeroDelayConstInterval(absl::Seconds(15))}; | |
| } | |
| struct DatabasePaths { | |
| std::filesystem::path fuzztest_db_path; | |
| std::filesystem::path regression_dir; | |
| std::filesystem::path coverage_dir; | |
| std::filesystem::path crashing_dir; | |
| std::filesystem::path incubating_dir; | |
| }; | |
| DatabasePaths GetDatabasePaths(const Environment& env) { | |
| const auto fuzztest_db_path = | |
| std::filesystem::path(env.fuzztest_corpus_database) / | |
| env.fuzztest_binary_identifier / env.test_name; | |
| return { | |
| fuzztest_db_path, | |
| fuzztest_db_path / "regression", | |
| fuzztest_db_path / "coverage", | |
| fuzztest_db_path / "crashing", | |
| fuzztest_db_path / "incubating", | |
| }; | |
| } | |
| void RecordFuzzingResults(const Environment& env, const DatabasePaths& db_paths, | |
| CentipedeCallbacksFactory& callbacks_factory, | |
| StopCondition& stop_condition) { | |
| const WorkDir workdir{env}; | |
| const bool update_crashing_db = | |
| env.fuzztest_update_corpus_database.value_or(!env.fuzztest_only_replay); | |
| const bool update_coverage_db = | |
| update_crashing_db && !env.fuzztest_only_replay; | |
| if (!update_crashing_db && !env.report_crash_summary) { | |
| return; | |
| } | |
| // Deduplicate and optionally update the crashing inputs. | |
| CrashSummary crash_summary{env.fuzztest_binary_identifier, env.test_name}; | |
| const absl::flat_hash_map<std::string, CrashDetails> crashes_by_signature = | |
| GetCrashesFromWorkdir(workdir, env.total_shards); | |
| if (update_crashing_db) { | |
| const absl::Status status = OrganizeCrashingInputs( | |
| db_paths.regression_dir, db_paths.crashing_dir, db_paths.incubating_dir, | |
| env, callbacks_factory, crashes_by_signature, crash_summary, | |
| stop_condition, kDefaultRegressionTtl); | |
| FUZZTEST_LOG_IF(ERROR, !status.ok()) | |
| << "Failed to organize crashing inputs: " << status; | |
| if (env.report_crash_summary) crash_summary.Report(&std::cerr); | |
| } else if (env.report_crash_summary) { | |
| // Just report the crashes. | |
| for (const auto& [crash_signature, crash_details] : crashes_by_signature) { | |
| crash_summary.AddCrash({/*id=*/crash_details.input_signature, | |
| /*category=*/crash_details.description, | |
| crash_signature, crash_details.description}); | |
| } | |
| crash_summary.Report(&std::cerr); | |
| } | |
| if (update_coverage_db) { | |
| // Distill and store the coverage corpus. | |
| Distill(env); | |
| if (RemotePathExists(db_paths.coverage_dir.c_str())) { | |
| // In the future, we will store k latest coverage corpora for some k, but | |
| // for now we only keep the latest one. | |
| FUZZTEST_CHECK_OK(RemotePathDelete(db_paths.coverage_dir.c_str(), | |
| /*recursively=*/true)); | |
| } | |
| FUZZTEST_CHECK_OK(RemoteMkdir(db_paths.coverage_dir.c_str())); | |
| std::vector<std::string> distilled_corpus_files; | |
| FUZZTEST_CHECK_OK( | |
| RemoteGlobMatch(workdir.DistilledCorpusFilePaths().AllShardsGlob(), | |
| distilled_corpus_files)); | |
| for (const std::string& corpus_file : distilled_corpus_files) { | |
| const std::string file_name = | |
| std::filesystem::path(corpus_file).filename(); | |
| FUZZTEST_CHECK_OK(RemoteFileRename( | |
| corpus_file, (db_paths.coverage_dir / file_name).c_str())); | |
| } | |
| } | |
| } | |
| void UpdateCorpusDatabase(Environment env, | |
| CentipedeCallbacksFactory& callbacks_factory, | |
| StopCondition& stop_condition) { | |
| FUZZTEST_LOG(INFO) << "Starting the update of the corpus database for:" | |
| << "\nFuzz test: " << env.test_name | |
| << "\nBinary: " << env.binary | |
| << "\nCorpus database: " << env.fuzztest_corpus_database; | |
| // Step 1: Preliminary set up of test sharding, binary info, etc. | |
| const auto [test_shard_index, total_test_shards] = SetUpTestSharding(); | |
| const auto corpus_database_path = | |
| std::filesystem::path(env.fuzztest_corpus_database) / | |
| env.fuzztest_binary_identifier; | |
| const auto stats_root_path = | |
| env.fuzztest_stats_root.empty() | |
| ? std::filesystem::path() | |
| : std::filesystem::path(env.fuzztest_stats_root) / | |
| env.fuzztest_binary_identifier; | |
| const auto workdir_root_path = | |
| env.fuzztest_workdir_root.empty() | |
| ? corpus_database_path | |
| : std::filesystem::path(env.fuzztest_workdir_root) / | |
| env.fuzztest_binary_identifier; | |
| const auto execution_stamp = [] { | |
| std::string stamp = | |
| absl::FormatTime("%Y-%m-%d-%H-%M-%S", absl::Now(), absl::UTCTimeZone()); | |
| return stamp; | |
| }(); | |
| const bool is_workdir_specified = !env.workdir.empty(); | |
| // When env.workdir is empty, the full workdir paths will be formed by | |
| // appending the fuzz test names to the base workdir path. We use different | |
| // path when only replaying to avoid replaying an unfinished fuzzing sessions. | |
| const auto base_workdir_path = | |
| is_workdir_specified | |
| ? std::filesystem::path{} // Will not be used. | |
| : workdir_root_path / | |
| absl::StrFormat("workdir%s.%03d", | |
| env.fuzztest_only_replay ? "-replay" : "", | |
| test_shard_index); | |
| // There's no point in saving the binary info to the workdir, since the | |
| // workdir is deleted at the end. | |
| env.save_binary_info = false; | |
| std::string pcs_file_path; | |
| BinaryInfo binary_info = PopulateBinaryInfoAndSavePCsIfNecessary( | |
| env, callbacks_factory, pcs_file_path, stop_condition); | |
| if (stop_condition.StopRequested()) return; | |
| FUZZTEST_LOG(INFO) << "Test shard index: " << test_shard_index | |
| << " Total test shards: " << total_test_shards; | |
| StopCondition::StopRequest stop_request; | |
| // Step 2: Run the fuzz test. | |
| // Unset stop time. stop_time will be set later. | |
| stop_condition.SetStopTime(absl::InfiniteFuture()); | |
| if (!is_workdir_specified) { | |
| env.workdir = base_workdir_path / env.test_name; | |
| } | |
| const auto execution_id_path = | |
| (base_workdir_path / absl::StrCat(env.test_name, ".execution_id")) | |
| .string(); | |
| bool is_resuming = false; | |
| if (!is_workdir_specified && !env.fuzztest_execution_id.empty()) { | |
| // Use the execution IDs to resume or skip tests. | |
| const bool execution_id_matched = [&] { | |
| if (!RemotePathExists(execution_id_path)) return false; | |
| FUZZTEST_CHECK(!RemotePathIsDirectory(execution_id_path)); | |
| std::string prev_execution_id; | |
| FUZZTEST_CHECK_OK( | |
| RemoteFileGetContents(execution_id_path, prev_execution_id)); | |
| return prev_execution_id == env.fuzztest_execution_id; | |
| }(); | |
| if (execution_id_matched) { | |
| // If execution IDs match but the previous coverage is missing, it means | |
| // the test was previously finished, and we skip running for the test. | |
| if (!RemotePathExists(WorkDir{env}.CoverageDirPath())) { | |
| FUZZTEST_LOG(INFO) << "Skipping running the fuzz test " | |
| << env.test_name; | |
| return; | |
| } | |
| // If execution IDs match and the previous coverage exists, it means | |
| // the same workflow got interrupted when running the test. So we resume | |
| // the test. | |
| is_resuming = true; | |
| FUZZTEST_LOG(INFO) << "Resuming running the fuzz test " << env.test_name; | |
| } else { | |
| // If the execution IDs mismatch, we start a new run. | |
| is_resuming = false; | |
| FUZZTEST_LOG(INFO) << "Starting a new run of the fuzz test " | |
| << env.test_name; | |
| } | |
| } | |
| if (!is_workdir_specified && RemotePathExists(env.workdir) && !is_resuming) { | |
| // This could be a workdir from a failed run that used a different version | |
| // of the binary. We delete it so that we don't have to deal with | |
| // the assumptions under which it is safe to reuse an old workdir. | |
| FUZZTEST_CHECK_OK(RemotePathDelete(env.workdir, /*recursively=*/true)); | |
| } | |
| const WorkDir workdir{env}; | |
| FUZZTEST_CHECK_OK(RemoteMkdir( | |
| workdir.CoverageDirPath())); // Implicitly creates the workdir | |
| // Updating execution ID must be after creating the coverage dir. Otherwise | |
| // if it fails to create coverage dir after updating execution ID, next | |
| // attempt would skip this test. | |
| if (!is_workdir_specified && !env.fuzztest_execution_id.empty() && | |
| !is_resuming) { | |
| FUZZTEST_CHECK_OK( | |
| RemoteFileSetContents(execution_id_path, env.fuzztest_execution_id)); | |
| } | |
| absl::Cleanup clean_up_workdir = [is_workdir_specified, &env, | |
| &stop_condition] { | |
| if (!is_workdir_specified && !stop_condition.StopRequested()) { | |
| FUZZTEST_CHECK_OK(RemotePathDelete(env.workdir, /*recursively=*/true)); | |
| } | |
| }; | |
| const DatabasePaths db_paths = GetDatabasePaths(env); | |
| // Seed the fuzzing session with the latest coverage corpus and regression | |
| // inputs from the previous fuzzing session. | |
| if (!is_resuming) { | |
| FUZZTEST_CHECK_OK(GenerateSeedCorpusFromConfig( | |
| GetSeedCorpusConfig(env, db_paths.regression_dir.c_str(), | |
| env.fuzztest_replay_coverage_inputs | |
| ? db_paths.coverage_dir.c_str() | |
| : ""), | |
| env.binary_name, env.binary_hash)) | |
| << "while generating the seed corpus"; | |
| } | |
| absl::Duration time_limit = env.fuzztest_time_limit_per_test; | |
| absl::Duration time_spent = absl::ZeroDuration(); | |
| const std::string fuzzing_time_file = | |
| std::filesystem::path(env.workdir) / "fuzzing_time"; | |
| if (is_resuming && RemotePathExists(fuzzing_time_file)) { | |
| time_spent = ReadFuzzingTime(fuzzing_time_file); | |
| time_limit = std::max(time_limit - time_spent, absl::ZeroDuration()); | |
| } | |
| is_resuming = false; | |
| if (stop_condition.StopRequested(&stop_request)) { | |
| if (stop_request.exit_code != EXIT_SUCCESS) { | |
| FUZZTEST_LOG(ERROR) << "Early stop requested for test " << env.test_name | |
| << " with failure exit code " | |
| << stop_request.exit_code; | |
| } else { | |
| FUZZTEST_LOG(INFO) << "Skipping test " << env.test_name | |
| << " due to early stop requested without failure."; | |
| } | |
| return; | |
| } | |
| FUZZTEST_LOG(INFO) << (env.fuzztest_only_replay ? "Replaying " : "Fuzzing ") | |
| << env.test_name << " for " << time_limit | |
| << "\n\tTest binary: " << env.binary; | |
| const absl::Time start_time = absl::Now(); | |
| stop_condition.SetStopTime(start_time + time_limit); | |
| PeriodicAction record_fuzzing_time = | |
| RecordFuzzingTime(fuzzing_time_file, start_time - time_spent); | |
| Fuzz(env, binary_info, pcs_file_path, callbacks_factory, stop_condition); | |
| record_fuzzing_time.Nudge(); | |
| record_fuzzing_time.Stop(); | |
| if (!stats_root_path.empty()) { | |
| const auto stats_dir = stats_root_path / env.test_name; | |
| FUZZTEST_CHECK_OK(RemoteMkdir(stats_dir.c_str())); | |
| FUZZTEST_CHECK_OK(RemoteFileRename( | |
| workdir.FuzzingStatsPath(), | |
| (stats_dir / absl::StrCat("fuzzing_stats_", execution_stamp)).c_str())); | |
| } | |
| if (stop_condition.StopRequested(&stop_request)) { | |
| if (stop_request.exit_code != EXIT_SUCCESS) { | |
| FUZZTEST_LOG(ERROR) << "Early stop requested for test " << env.test_name | |
| << " with failure exit code " | |
| << stop_request.exit_code; | |
| } else { | |
| FUZZTEST_LOG(INFO) << "Skip updating corpus database due to early stop " | |
| "requested without failure."; | |
| } | |
| return; | |
| } | |
| // The test time limit does not apply for updating the corpus database. | |
| stop_condition.SetStopTime(absl::InfiniteFuture()); | |
| RecordFuzzingResults(env, db_paths, callbacks_factory, stop_condition); | |
| } | |
| int ListCrashIds(const Environment& env) { | |
| FUZZTEST_CHECK(!env.list_crash_ids_file.empty()) | |
| << "Need list_crash_ids_file to be set for listing crash IDs"; | |
| FUZZTEST_CHECK(!env.test_name.empty()); | |
| std::vector<std::string> crash_paths; | |
| // TODO: b/406003594 - move the path construction to a library. | |
| const auto crash_dir = GetDatabasePaths(env).crashing_dir; | |
| if (RemotePathExists(crash_dir.string())) { | |
| FUZZTEST_CHECK(RemotePathIsDirectory(crash_dir.string())) | |
| << "Crash dir " << crash_dir << " in the corpus database " | |
| << env.fuzztest_corpus_database << " is not a directory"; | |
| crash_paths = | |
| ValueOrDie(RemoteListFiles(crash_dir.string(), /*recursively=*/false)); | |
| } | |
| std::vector<std::string> results; | |
| results.reserve(crash_paths.size()); | |
| for (const auto& crash_path : crash_paths) { | |
| std::string crash_id = std::filesystem::path{crash_path}.filename(); | |
| results.push_back(std::move(crash_id)); | |
| } | |
| FUZZTEST_CHECK_OK(RemoteFileSetContents(env.list_crash_ids_file, | |
| absl::StrJoin(results, "\n"))); | |
| return EXIT_SUCCESS; | |
| } | |
| void ReplayCrash(const Environment& env, | |
| CentipedeCallbacksFactory& callbacks_factory, | |
| StopCondition& stop_condition) { | |
| FUZZTEST_CHECK(!env.crash_id.empty()) | |
| << "Need crash_id to be set for replay a crash"; | |
| FUZZTEST_CHECK(!env.test_name.empty()) | |
| << "Expecting exactly one test for replay_crash"; | |
| // TODO: b/406003594 - move the path construction to a library. | |
| const auto crash_dir = std::filesystem::path(env.fuzztest_corpus_database) / | |
| env.fuzztest_binary_identifier / env.test_name / | |
| "crashing"; | |
| const WorkDir workdir{env}; | |
| SeedCorpusSource crash_corpus_source; | |
| crash_corpus_source.dir_glob = crash_dir; | |
| crash_corpus_source.num_recent_dirs = 1; | |
| crash_corpus_source.individual_input_rel_glob = env.crash_id; | |
| crash_corpus_source.sampled_fraction_or_count = 1.0f; | |
| const SeedCorpusConfig crash_corpus_config = { | |
| /*sources=*/{crash_corpus_source}, | |
| /*destination=*/{ | |
| /*dir_path=*/env.workdir, | |
| /*shard_rel_glob=*/ | |
| std::filesystem::path{workdir.CorpusFilePaths().AllShardsGlob()} | |
| .filename(), | |
| /*shard_index_digits=*/WorkDir::kDigitsInShardIndex, | |
| /*num_shards=*/1}}; | |
| FUZZTEST_CHECK_OK(GenerateSeedCorpusFromConfig( | |
| crash_corpus_config, env.binary_name, env.binary_hash)); | |
| Environment run_crash_env = env; | |
| run_crash_env.load_shards_only = true; | |
| run_crash_env.persistent_mode = false; | |
| Fuzz(run_crash_env, {}, "", callbacks_factory, stop_condition); | |
| if (env.report_crash_summary) { | |
| CrashSummary crash_summary{env.fuzztest_binary_identifier, env.test_name}; | |
| const absl::flat_hash_map<std::string, CrashDetails> crashes_by_signature = | |
| GetCrashesFromWorkdir(workdir, /*total_shards=*/1); | |
| FUZZTEST_CHECK_LE(crashes_by_signature.size(), 1); | |
| for (const auto& [signature, crash_details] : crashes_by_signature) { | |
| crash_summary.AddCrash({env.crash_id, | |
| /*category=*/crash_details.description, signature, | |
| crash_details.description}); | |
| } | |
| crash_summary.Report(&std::cerr); | |
| } | |
| } | |
| int ExportCrash(const Environment& env) { | |
| FUZZTEST_CHECK(!env.crash_id.empty()) | |
| << "Need crash_id to be set for exporting a crash"; | |
| FUZZTEST_CHECK(!env.export_crash_file.empty()) | |
| << "Need export_crash_file to be set for exporting a crash"; | |
| FUZZTEST_CHECK(!env.test_name.empty()) | |
| << "Expecting exactly one test for exporting a crash"; | |
| // TODO: b/406003594 - move the path construction to a library. | |
| const auto crash_dir = std::filesystem::path(env.fuzztest_corpus_database) / | |
| env.fuzztest_binary_identifier / env.test_name / | |
| "crashing"; | |
| std::string crash_contents; | |
| const auto read_status = | |
| RemoteFileGetContents((crash_dir / env.crash_id).c_str(), crash_contents); | |
| if (!read_status.ok()) { | |
| FUZZTEST_LOG(ERROR) << "Failed reading the crash " << env.crash_id | |
| << " from " << crash_dir.c_str() << ": " << read_status; | |
| return EXIT_FAILURE; | |
| } | |
| const auto write_status = | |
| RemoteFileSetContents(env.export_crash_file, crash_contents); | |
| if (!write_status.ok()) { | |
| FUZZTEST_LOG(ERROR) << "Failed write the crash " << env.crash_id << " to " | |
| << env.export_crash_file << ": " << write_status; | |
| return EXIT_FAILURE; | |
| } | |
| return EXIT_SUCCESS; | |
| } | |
| } // namespace | |
| int CentipedeMain(const Environment& env, | |
| CentipedeCallbacksFactory& callbacks_factory, | |
| StopCondition* stop_condition) { | |
| StopCondition default_stop_condition; | |
| if (stop_condition == nullptr) { | |
| stop_condition = &default_stop_condition; | |
| } | |
| stop_condition->SetStopTime(env.stop_at); | |
| auto SaveStopReasonAndGetExitCode = [&]() -> int { | |
| StopCondition::StopRequest stop_request; | |
| const bool stop_requested = stop_condition->StopRequested(&stop_request); | |
| if (!stop_requested) return EXIT_SUCCESS; | |
| if (!env.stop_reason_file.empty()) { | |
| WriteToLocalFile(env.stop_reason_file, stop_request.reason); | |
| } | |
| return stop_request.exit_code; | |
| }; | |
| if (!env.corpus_to_files.empty()) { | |
| Centipede::CorpusToFiles(env, env.corpus_to_files); | |
| return EXIT_SUCCESS; | |
| } | |
| if (!env.crashes_to_files.empty()) { | |
| const auto status = Centipede::CrashesToFiles(env, env.crashes_to_files); | |
| if (status.ok()) return EXIT_SUCCESS; | |
| FUZZTEST_LOG(ERROR) << "Got error when exporting crashes to files: " | |
| << status; | |
| return EXIT_FAILURE; | |
| } | |
| if (!env.for_each_blob.empty()) { | |
| ForEachBlob(env, *stop_condition); | |
| return SaveStopReasonAndGetExitCode(); | |
| } | |
| if (!env.minimize_crash_file_path.empty()) { | |
| MinimizeCrash(env, callbacks_factory, *stop_condition); | |
| return SaveStopReasonAndGetExitCode(); | |
| } | |
| // Just export the corpus from a local dir and exit. | |
| if (!env.corpus_from_files.empty()) { | |
| Centipede::CorpusFromFiles(env, env.corpus_from_files); | |
| return EXIT_SUCCESS; | |
| } | |
| // Export the corpus from a local dir and then fuzz. | |
| if (!env.corpus_dir.empty()) { | |
| for (size_t i = 0; i < env.corpus_dir.size(); ++i) { | |
| const auto& corpus_dir = env.corpus_dir[i]; | |
| if (i > 0 || !env.first_corpus_dir_output_only) | |
| Centipede::CorpusFromFiles(env, corpus_dir); | |
| } | |
| } | |
| if (env.distill) return Distill(env); | |
| // Create the local temporary dir once, before creating any threads. The | |
| // temporary dir must typically exist before `CentipedeCallbacks` can be used. | |
| const auto tmpdir = TemporaryLocalDirPath(); | |
| CreateLocalDirRemovedAtExit(tmpdir); | |
| // Enter the update corpus database mode only if we have a binary to invoke | |
| // and a corpus database to update. | |
| // We don't update the corpus database for standalone binaries (i.e., when | |
| // `env.has_input_wildcards` is true). | |
| if (!env.binary.empty() && !env.has_input_wildcards) { | |
| const auto serialized_target_config = [&]() -> absl::StatusOr<std::string> { | |
| // TODO: b/410051414 Use Centipede flags to pass necessary information | |
| // instead of passing the entirely serialized Configuration once switched | |
| // to the unified execution model. | |
| if (env.fuzztest_configuration == "(null)") { | |
| return ""; | |
| } else if (!env.fuzztest_configuration.empty()) { | |
| std::string result; | |
| FUZZTEST_CHECK( | |
| absl::WebSafeBase64Unescape(env.fuzztest_configuration, &result)); | |
| return result; | |
| } | |
| ScopedCentipedeCallbacks scoped_callbacks(callbacks_factory, env, | |
| *stop_condition); | |
| return scoped_callbacks.callbacks()->GetSerializedTargetConfig(); | |
| }(); | |
| Environment updated_env = env; | |
| if (updated_env.fuzztest_corpus_database.empty()) { | |
| if (!serialized_target_config.ok()) { | |
| FUZZTEST_LOG(ERROR) << "Failed to get the serialized target config: " | |
| << serialized_target_config.status(); | |
| return EXIT_FAILURE; | |
| } | |
| if (!serialized_target_config->empty()) { | |
| const auto target_config = | |
| fuzztest::internal::Configuration::Deserialize( | |
| *serialized_target_config); | |
| FUZZTEST_CHECK_OK(target_config.status()) | |
| << "Failed to deserialize target configuration"; | |
| if (!target_config->corpus_database.empty()) { | |
| updated_env.UpdateWithTargetConfig(*target_config); | |
| } | |
| } | |
| } | |
| if (!updated_env.fuzztest_corpus_database.empty()) { | |
| FUZZTEST_LOG_IF( | |
| FATAL, env.list_crash_ids + env.replay_crash + env.export_crash > 1) | |
| << "At most one of list_crash_ids/replay_crash/export_crash can " | |
| "be set, but seeing list_crash_ids: " | |
| << env.list_crash_ids << ", replay_crash: " << env.replay_crash | |
| << ", export_crash: " << env.export_crash; | |
| if (env.list_crash_ids) { | |
| return ListCrashIds(updated_env); | |
| } | |
| if (env.replay_crash) { | |
| ReplayCrash(updated_env, callbacks_factory, *stop_condition); | |
| return SaveStopReasonAndGetExitCode(); | |
| } | |
| if (env.export_crash) { | |
| return ExportCrash(updated_env); | |
| } | |
| FUZZTEST_CHECK(!updated_env.fuzztest_multi_test_mode_soon_to_be_removed) | |
| << "Multi-test mode is no longer supported."; | |
| FUZZTEST_CHECK(!updated_env.test_name.empty()) | |
| << "Centipede requires the test name for FuzzTest."; | |
| FUZZTEST_CHECK(updated_env.fuzztest_time_limit_per_test >= | |
| absl::Seconds(1)) | |
| << "Time limit per fuzz test must be at least 1 second."; | |
| UpdateCorpusDatabase(updated_env, callbacks_factory, *stop_condition); | |
| return SaveStopReasonAndGetExitCode(); | |
| } | |
| } | |
| // Create the remote coverage dirs once, before creating any threads. | |
| const auto coverage_dir = WorkDir{env}.CoverageDirPath(); | |
| FUZZTEST_CHECK_OK(RemoteMkdir(coverage_dir)); | |
| FUZZTEST_LOG(INFO) << "Coverage dir: " << coverage_dir | |
| << "; temporary dir: " << tmpdir; | |
| std::string pcs_file_path; | |
| BinaryInfo binary_info = PopulateBinaryInfoAndSavePCsIfNecessary( | |
| env, callbacks_factory, pcs_file_path, *stop_condition); | |
| if (stop_condition->StopRequested()) { | |
| return SaveStopReasonAndGetExitCode(); | |
| } | |
| if (env.analyze) return Analyze(env); | |
| Fuzz(env, binary_info, pcs_file_path, callbacks_factory, *stop_condition); | |
| return SaveStopReasonAndGetExitCode(); | |
| // TODO: fniksic - Report the crash summary here if requested. What are the | |
| // binary identifier and the fuzz test name here? | |
| } | |
| } // namespace fuzztest::internal |