diff --git a/centipede/BUILD b/centipede/BUILD index 37d2a1d3..69d2f1b0 100644 --- a/centipede/BUILD +++ b/centipede/BUILD @@ -44,8 +44,8 @@ cc_binary( ":centipede_interface", ":config_file", ":environment_flags", + ":stop", "@abseil-cpp//absl/base:nullability", - "@abseil-cpp//absl/log:flags", ], ) @@ -898,6 +898,7 @@ cc_library( ":crash_summary", ":environment", ":runner_result", + ":stop", ":workdir", "@abseil-cpp//absl/container:flat_hash_map", "@abseil-cpp//absl/container:flat_hash_set", @@ -1281,6 +1282,7 @@ cc_library( ":feature", ":mutation_data", ":runner_result", + ":stop", ":util", "@abseil-cpp//absl/types:span", "@com_google_fuzztest//common:defs", @@ -1327,6 +1329,7 @@ cc_test( ":centipede_callbacks", ":environment", ":runner_result", + ":stop", "@abseil-cpp//absl/types:span", "@com_google_fuzztest//common:defs", "@googletest//:gtest_main", @@ -1502,6 +1505,7 @@ cc_test( ":environment", ":minimize_crash", ":runner_result", + ":stop", ":util", ":workdir", "@abseil-cpp//absl/base:nullability", @@ -1919,6 +1923,7 @@ cc_test( ":crash_summary", ":environment", ":runner_result", + ":stop", ":util", ":workdir", "@abseil-cpp//absl/container:flat_hash_map", diff --git a/centipede/centipede.cc b/centipede/centipede.cc index c8d09b04..e2a0ee33 100644 --- a/centipede/centipede.cc +++ b/centipede/centipede.cc @@ -115,8 +115,10 @@ std::vector InputsToMutantRefs(const std::vector& inputs) { Centipede::Centipede(const Environment& env, CentipedeCallbacks& user_callbacks, const BinaryInfo& binary_info, - CoverageLogger& coverage_logger, std::atomic& stats) + CoverageLogger& coverage_logger, std::atomic& 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. @@ -379,7 +381,7 @@ void Centipede::LogFeaturesAsSymbols(const FeatureVec &fv) { bool Centipede::InputPassesFilter(ByteSpan input) { if (env_.input_filter.empty()) return true; WriteToLocalFile(input_filter_path_, input); - bool result = input_filter_cmd_.Execute() == EXIT_SUCCESS; + bool result = input_filter_cmd_.Execute(&stop_condition_) == EXIT_SUCCESS; std::filesystem::remove(input_filter_path_); return result; } @@ -394,7 +396,7 @@ bool Centipede::ExecuteAndReportCrash(std::string_view binary, << batch_result.failure_description(); return true; } - if (ShouldStop()) { + if (stop_condition_.ShouldStop()) { FUZZTEST_LOG_FIRST_N(WARNING, 1) << "Crash found but the stop condition is met - not reporting further " "possibly related crashes."; @@ -460,20 +462,20 @@ bool Centipede::RunBatch( FUZZTEST_CHECK_EQ(mutants.size(), batch_result.results().size()); for (const auto &extra_binary : env_.extra_binaries) { - if (ShouldStop()) break; + if (stop_condition_.ShouldStop()) break; BatchResult extra_batch_result; success = ExecuteAndReportCrash(extra_binary, inputs, extra_batch_result) && success; } - if (EarlyStopRequested()) return false; + if (stop_condition_.EarlyStopRequested()) return false; if (!success && env_.exit_on_crash) { FUZZTEST_LOG(INFO) << "--exit_on_crash is enabled; exiting soon"; - RequestEarlyStop(EXIT_FAILURE); + stop_condition_.RequestEarlyStop(EXIT_FAILURE); return false; } bool batch_gained_new_coverage = false; for (size_t i = 0; i < mutants.size(); i++) { - if (ShouldStop()) break; + 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; @@ -555,7 +557,7 @@ void Centipede::LoadShard(const Environment &load_env, size_t shard_index, std::vector inputs_to_rerun; auto input_features_callback = [&](ByteArray input, FeatureVec input_features) { - if (ShouldStop()) return; + if (stop_condition_.ShouldStop()) return; if (input_features.empty()) { if (rerun) { inputs_to_rerun.emplace_back(std::move(input)); @@ -627,7 +629,7 @@ void Centipede::Rerun(std::vector &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; + if (stop_condition_.ShouldStop()) break; size_t batch_size = std::min(to_rerun.size(), env_.batch_size); if (RunBatch( InputsToMutantRefs({to_rerun.end() - batch_size, to_rerun.end()}), @@ -903,7 +905,7 @@ void Centipede::FuzzingLoop() { 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; + 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); @@ -923,7 +925,7 @@ void Centipede::FuzzingLoop() { std::vector mutants = user_callbacks_.Mutate(mutation_inputs, batch_size); - if (ShouldStop()) break; + if (stop_condition_.ShouldStop()) break; new_runs += mutants.size(); std::vector mutant_refs; @@ -1015,7 +1017,7 @@ void Centipede::ReportCrash(std::string_view binary, if (batch_result.IsSkippedTest()) { log_execution_failure("Skipped Test: "); FUZZTEST_LOG(INFO) << "Requesting early stop due to skipped test."; - RequestEarlyStop(EXIT_SUCCESS); + stop_condition_.RequestEarlyStop(EXIT_SUCCESS); return; } @@ -1023,13 +1025,13 @@ void Centipede::ReportCrash(std::string_view binary, log_execution_failure("Test Setup Failure: "); FUZZTEST_LOG(INFO) << "Requesting early stop due to setup failure in the test."; - RequestEarlyStop(EXIT_FAILURE); + stop_condition_.RequestEarlyStop(EXIT_FAILURE); return; } // Skip reporting only if RequestEarlyStop is called - still reporting if time - // runs out. - if (EarlyStopRequested()) return; + // limit is reached. + if (stop_condition_.EarlyStopRequested()) return; if (++num_crashes_ > env_.max_num_crash_reports) return; @@ -1073,14 +1075,14 @@ void Centipede::ReportCrash(std::string_view binary, << log_prefix << "Executing inputs one-by-one, trying to find the reproducer"; for (auto input_idx : input_idxs_to_try) { - if (ShouldStop()) break; + if (stop_condition_.ShouldStop()) break; const auto one_input = input_vec[input_idx]; 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() && - !ShouldStop()) { + !stop_condition_.ShouldStop()) { auto hash = Hash(one_input); auto crash_dir = wd_.CrashReproducerDirPaths().MyShard(); FUZZTEST_CHECK_OK(RemoteMkdir(crash_dir)); @@ -1113,7 +1115,7 @@ void Centipede::ReportCrash(std::string_view binary, } } - if (ShouldStop()) { + if (stop_condition_.ShouldStop()) { FUZZTEST_LOG(INFO) << log_prefix << "Stop condition is on - skipping triaging the reproducer"; diff --git a/centipede/centipede.h b/centipede/centipede.h index 18fa83d8..0ce73390 100644 --- a/centipede/centipede.h +++ b/centipede/centipede.h @@ -38,6 +38,7 @@ #include "./centipede/runner_result.h" #include "./centipede/rusage_profiler.h" #include "./centipede/stats.h" +#include "./centipede/stop.h" #include "./centipede/symbol_table.h" #include "./centipede/workdir.h" #include "./common/blob_file.h" @@ -48,9 +49,9 @@ namespace fuzztest::internal { // The main fuzzing class. class Centipede { public: - Centipede(const Environment &env, CentipedeCallbacks &user_callbacks, - const BinaryInfo &binary_info, CoverageLogger &coverage_logger, - std::atomic &stats); + Centipede(const Environment& env, CentipedeCallbacks& user_callbacks, + const BinaryInfo& binary_info, CoverageLogger& coverage_logger, + std::atomic& stats, StopCondition& stop_condition); virtual ~Centipede() = default; // Non-copyable and non-movable. @@ -175,6 +176,7 @@ class Centipede { size_t AddPcPairFeatures(FeatureVec &fv); const Environment &env_; + StopCondition& stop_condition_; const WorkDir wd_{env_}; CentipedeCallbacks &user_callbacks_; diff --git a/centipede/centipede_callbacks.cc b/centipede/centipede_callbacks.cc index e5603697..b03b48aa 100644 --- a/centipede/centipede_callbacks.cc +++ b/centipede/centipede_callbacks.cc @@ -469,7 +469,7 @@ int CentipedeCallbacks::RunBatchForBinary(std::string_view binary) { ? absl::InfiniteDuration() : absl::Seconds(env_.timeout_per_batch) + absl::Seconds(5); const auto deadline = - std::min(absl::Now() + amortized_timeout, GetStopTime()); + std::min(absl::Now() + amortized_timeout, stop_condition_.GetStopTime()); int exit_code = EXIT_SUCCESS; const bool should_clean_up = [&] { if (!cmd.is_executing()) { @@ -491,7 +491,7 @@ int CentipedeCallbacks::RunBatchForBinary(std::string_view binary) { } return false; } - const std::optional ret = cmd.Wait(deadline); + const std::optional ret = cmd.Wait(deadline, &stop_condition_); if (!ret.has_value()) return true; exit_code = *ret; return false; @@ -660,7 +660,8 @@ bool CentipedeCallbacks::GetSeedsViaExternalBinary( << cmd.ToString(); return EXIT_FAILURE; } - const auto wait_result = cmd.Wait(GetStopTime()); + const auto wait_result = + cmd.Wait(stop_condition_.GetStopTime(), &stop_condition_); if (!wait_result.has_value()) { FUZZTEST_LOG(ERROR) << "Failed to wait for the seeding command " << cmd.ToString(); diff --git a/centipede/centipede_callbacks.h b/centipede/centipede_callbacks.h index a4fb2bb4..38d6bc35 100644 --- a/centipede/centipede_callbacks.h +++ b/centipede/centipede_callbacks.h @@ -34,6 +34,7 @@ #include "./centipede/mutation_data.h" #include "./centipede/runner_result.h" #include "./centipede/shared_memory_blob_sequence.h" +#include "./centipede/stop.h" #include "./centipede/util.h" #include "./common/defs.h" #include "./common/logging.h" @@ -47,9 +48,9 @@ namespace fuzztest::internal { // Note: the interface is not yet stable and may change w/o a notice. class CentipedeCallbacks { public: - // `env` is used to pass flags to `this`, it must outlive `this`. - CentipedeCallbacks(const Environment &env) + CentipedeCallbacks(const Environment& env, StopCondition& stop_condition) : env_(env), + stop_condition_(stop_condition), byte_array_mutator_(env.knobs, GetRandomSeed(env.seed)), fuzztest_mutator_(env.knobs, GetRandomSeed(env.seed)), inputs_blobseq_(shmem_name1_.c_str(), env.shmem_size_mb << 20, @@ -164,6 +165,7 @@ class CentipedeCallbacks { void PrintExecutionLog() const; const Environment &env_; + StopCondition& stop_condition_; ByteArrayMutator byte_array_mutator_; FuzzTestMutator fuzztest_mutator_; @@ -216,7 +218,8 @@ class CentipedeCallbacks { // and not actually delete it. class CentipedeCallbacksFactory { public: - virtual CentipedeCallbacks *create(const Environment &env) = 0; + virtual CentipedeCallbacks* create(const Environment& env, + StopCondition& stop_condition) = 0; virtual void destroy(CentipedeCallbacks *callbacks) = 0; virtual ~CentipedeCallbacksFactory() {} }; @@ -225,8 +228,9 @@ class CentipedeCallbacksFactory { template class DefaultCallbacksFactory : public CentipedeCallbacksFactory { public: - CentipedeCallbacks *create(const Environment &env) override { - return new Type(env); + CentipedeCallbacks* create(const Environment& env, + StopCondition& stop_condition) override { + return new Type(env, stop_condition); } void destroy(CentipedeCallbacks *callbacks) override { delete callbacks; } }; @@ -239,7 +243,8 @@ class NonOwningCallbacksFactory : public CentipedeCallbacksFactory { public: explicit NonOwningCallbacksFactory(CentipedeCallbacks& callbacks) : callbacks_(callbacks) {} - CentipedeCallbacks* absl_nonnull create(const Environment& env) override { + CentipedeCallbacks* absl_nonnull create( + const Environment& env, StopCondition& stop_condition) override { const bool was_already_created = is_created_.exchange(true, std::memory_order_acq_rel); FUZZTEST_CHECK(!was_already_created) @@ -263,9 +268,10 @@ class NonOwningCallbacksFactory : public CentipedeCallbacksFactory { // Creates a CentipedeCallbacks object in CTOR and destroys it in DTOR. class ScopedCentipedeCallbacks { public: - ScopedCentipedeCallbacks(CentipedeCallbacksFactory &factory, - const Environment &env) - : factory_(factory), callbacks_(factory_.create(env)) {} + ScopedCentipedeCallbacks(CentipedeCallbacksFactory& factory, + const Environment& env, + StopCondition& stop_condition) + : factory_(factory), callbacks_(factory_.create(env, stop_condition)) {} ~ScopedCentipedeCallbacks() { factory_.destroy(callbacks_); } CentipedeCallbacks *absl_nonnull callbacks() { return callbacks_; } diff --git a/centipede/centipede_callbacks_test.cc b/centipede/centipede_callbacks_test.cc index 40a5fc1b..31764153 100644 --- a/centipede/centipede_callbacks_test.cc +++ b/centipede/centipede_callbacks_test.cc @@ -20,6 +20,7 @@ #include "absl/types/span.h" #include "./centipede/environment.h" #include "./centipede/runner_result.h" +#include "./centipede/stop.h" #include "./common/defs.h" namespace fuzztest::internal { @@ -27,26 +28,33 @@ namespace { class FakeCallbacks : public CentipedeCallbacks { public: - explicit FakeCallbacks(const Environment& env) : CentipedeCallbacks(env) {} + explicit FakeCallbacks(const Environment& env) + : CentipedeCallbacks(env, mock_stop_condition_) {} bool Execute(std::string_view binary, absl::Span inputs, BatchResult& batch_result) override { return true; } + + private: + StopCondition mock_stop_condition_; }; TEST(NonOwningCallbacksFactoryTest, CreateReturnsUnderlyingCallbacks) { Environment env; FakeCallbacks callbacks(env); NonOwningCallbacksFactory factory(callbacks); - EXPECT_EQ(factory.create(env), &callbacks); + StopCondition stop_condition; + EXPECT_EQ(factory.create(env, stop_condition), &callbacks); } TEST(NonOwningCallbacksFactoryTest, CannotCreateTwice) { Environment env; FakeCallbacks callbacks(env); NonOwningCallbacksFactory factory(callbacks); - factory.create(env); - EXPECT_DEATH(factory.create(env), "create\\(\\) called before destroy\\(\\)"); + StopCondition stop_condition; + factory.create(env, stop_condition); + EXPECT_DEATH(factory.create(env, stop_condition), + "create\\(\\) called before destroy\\(\\)"); } TEST(NonOwningCallbacksFactoryTest, CannotDestroyBeforeCreate) { @@ -61,7 +69,8 @@ TEST(NonOwningCallbacksFactoryTest, CannotDestroyTwice) { Environment env; FakeCallbacks callbacks(env); NonOwningCallbacksFactory factory(callbacks); - factory.create(env); + StopCondition stop_condition; + factory.create(env, stop_condition); factory.destroy(&callbacks); EXPECT_DEATH(factory.destroy(&callbacks), "destroy\\(\\) called before the matching create\\(\\)"); diff --git a/centipede/centipede_default_callbacks.cc b/centipede/centipede_default_callbacks.cc index 3f071745..d8b01a35 100644 --- a/centipede/centipede_default_callbacks.cc +++ b/centipede/centipede_default_callbacks.cc @@ -34,8 +34,9 @@ namespace fuzztest::internal { -CentipedeDefaultCallbacks::CentipedeDefaultCallbacks(const Environment &env) - : CentipedeCallbacks(env) { +CentipedeDefaultCallbacks::CentipedeDefaultCallbacks( + const Environment& env, StopCondition& stop_condition) + : CentipedeCallbacks(env, stop_condition) { for (const auto &dictionary_path : env_.dictionary) { LoadDictionary(dictionary_path); } @@ -106,7 +107,7 @@ std::vector CentipedeDefaultCallbacks::Mutate( "generate some using the built-in mutator."; } break; - } else if (ShouldStop()) { + } else if (stop_condition_.ShouldStop()) { FUZZTEST_LOG(WARNING) << "Custom mutator failed, but ignored since the stop " "condition it met. Possibly what triggered the stop " @@ -123,7 +124,7 @@ std::vector CentipedeDefaultCallbacks::Mutate( PrintExecutionLog(); FUZZTEST_LOG(ERROR) << "Test binary failed to mutate inputs at the final " "attempt - exiting."; - RequestEarlyStop(EXIT_FAILURE); + stop_condition_.RequestEarlyStop(EXIT_FAILURE); return {}; } } diff --git a/centipede/centipede_default_callbacks.h b/centipede/centipede_default_callbacks.h index 02fff74f..d5e7b75f 100644 --- a/centipede/centipede_default_callbacks.h +++ b/centipede/centipede_default_callbacks.h @@ -31,6 +31,7 @@ #include "./centipede/environment.h" #include "./centipede/mutation_data.h" #include "./centipede/runner_result.h" +#include "./centipede/stop.h" #include "./common/defs.h" namespace fuzztest::internal { @@ -38,7 +39,8 @@ namespace fuzztest::internal { // Example of customized CentipedeCallbacks. class CentipedeDefaultCallbacks : public CentipedeCallbacks { public: - explicit CentipedeDefaultCallbacks(const Environment &env); + CentipedeDefaultCallbacks(const Environment& env, + StopCondition& stop_condition); size_t GetSeeds(size_t num_seeds, std::vector &seeds) override; absl::StatusOr GetSerializedTargetConfig() override; bool Execute(std::string_view binary, absl::Span inputs, diff --git a/centipede/centipede_interface.cc b/centipede/centipede_interface.cc index e07ca4b6..c519be11 100644 --- a/centipede/centipede_interface.cc +++ b/centipede/centipede_interface.cc @@ -79,23 +79,10 @@ namespace { // Sets signal handler for SIGINT. // TODO(b/378532202): Replace this with a more generic mechanism that allows // the called or `CentipedeMain()` to indicate when to stop. -void SetSignalHandlers() { - struct sigaction sigact = {}; - sigact.sa_flags = SA_ONSTACK; - sigact.sa_handler = [](int received_signum) { - if (received_signum == SIGINT) { - FUZZTEST_LOG(INFO) << "Ctrl-C pressed: winding down"; - RequestEarlyStop(EXIT_FAILURE); - return; - } - ABSL_UNREACHABLE(); - }; - sigaction(SIGINT, &sigact, nullptr); -} // Runs env.for_each_blob on every blob extracted from env.args. // Returns EXIT_SUCCESS on success, EXIT_FAILURE otherwise. -int ForEachBlob(const Environment& env) { +int ForEachBlob(const Environment& env, StopCondition& stop_condition) { auto tmpdir = TemporaryLocalDirPath(); CreateLocalDirRemovedAtExit(tmpdir); std::string tmpfile = std::filesystem::path(tmpdir).append("t"); @@ -118,7 +105,7 @@ int ForEachBlob(const Environment& env) { // 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 (ShouldStop()) return ExitCode(); + if (stop_condition.ShouldStop()) return stop_condition.ExitCode(); } } return EXIT_SUCCESS; @@ -154,12 +141,13 @@ void SavePCTableToFile(const PCTable& pc_table, std::string_view file_path) { BinaryInfo PopulateBinaryInfoAndSavePCsIfNecessary( const Environment& env, CentipedeCallbacksFactory& callbacks_factory, - std::string& pcs_file_path) { + 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); + ScopedCentipedeCallbacks scoped_callbacks(callbacks_factory, env, + stop_condition); scoped_callbacks.callbacks()->PopulateBinaryInfo(binary_info); } if (env.save_binary_info) { @@ -193,7 +181,8 @@ std::vector CreateEnvironmentsForThreads( int Fuzz(const Environment& env, const BinaryInfo& binary_info, std::string_view pcs_file_path, - CentipedeCallbacksFactory& callbacks_factory) { + CentipedeCallbacksFactory& callbacks_factory, + StopCondition& stop_condition) { CoverageLogger coverage_logger(binary_info.pc_table, binary_info.symbols); std::vector envs = @@ -225,20 +214,22 @@ int Fuzz(const Environment& env, const BinaryInfo& binary_info, }); } - auto fuzzing_worker = - [&env, &callbacks_factory, &binary_info, &coverage_logger]( - Environment& my_env, std::atomic& stats, bool create_tmpdir) { - if (create_tmpdir) CreateLocalDirRemovedAtExit(TemporaryLocalDirPath()); - // Uses TID, call in this thread. - my_env.seed = GetRandomSeed(env.seed); + auto fuzzing_worker = [&env, &callbacks_factory, &binary_info, + &coverage_logger, &stop_condition]( + Environment& my_env, std::atomic& 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; + if (env.dry_run) return; - ScopedCentipedeCallbacks scoped_callbacks(callbacks_factory, my_env); - Centipede centipede(my_env, *scoped_callbacks.callbacks(), binary_info, - coverage_logger, stats); - centipede.FuzzingLoop(); - }; + 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 @@ -268,7 +259,7 @@ int Fuzz(const Environment& env, const BinaryInfo& binary_info, if (!env.knobs_file.empty()) PrintRewardValues(stats_vec, std::cerr); - return ExitCode(); + return stop_condition.ExitCode(); } TestShard SetUpTestSharding() { @@ -351,7 +342,8 @@ PeriodicAction RecordFuzzingTime(std::string_view fuzzing_time_file, } int UpdateCorpusDatabase(Environment env, - CentipedeCallbacksFactory& callbacks_factory) { + 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 @@ -394,7 +386,7 @@ int UpdateCorpusDatabase(Environment env, env.save_binary_info = false; std::string pcs_file_path; BinaryInfo binary_info = PopulateBinaryInfoAndSavePCsIfNecessary( - env, callbacks_factory, pcs_file_path); + env, callbacks_factory, pcs_file_path, stop_condition); FUZZTEST_LOG(INFO) << "Test shard index: " << test_shard_index << " Total test shards: " << total_test_shards; @@ -404,7 +396,8 @@ int UpdateCorpusDatabase(Environment env, // Step 2: Run the fuzz test. // Clean up previous stop requests. stop_time will be set later. - ClearEarlyStopRequestAndSetStopTime(/*stop_time=*/absl::InfiniteFuture()); + stop_condition.ClearEarlyStopRequestAndSetStopTime( + /*stop_time=*/absl::InfiniteFuture()); if (!is_workdir_specified) { env.workdir = base_workdir_path / env.test_name; @@ -464,8 +457,9 @@ int UpdateCorpusDatabase(Environment env, RemoteFileSetContents(execution_id_path, env.fuzztest_execution_id)); } - absl::Cleanup clean_up_workdir = [is_workdir_specified, &env] { - if (!is_workdir_specified && !EarlyStopRequested()) { + absl::Cleanup clean_up_workdir = [is_workdir_specified, &env, + &stop_condition] { + if (!is_workdir_specified && !stop_condition.EarlyStopRequested()) { FUZZTEST_CHECK_OK(RemotePathDelete(env.workdir, /*recursively=*/true)); } }; @@ -496,9 +490,9 @@ int UpdateCorpusDatabase(Environment env, } is_resuming = false; - if (EarlyStopRequested()) { - if (ExitCode() != EXIT_SUCCESS) { - exit_code = ExitCode(); + if (stop_condition.EarlyStopRequested()) { + if (stop_condition.ExitCode() != EXIT_SUCCESS) { + exit_code = stop_condition.ExitCode(); FUZZTEST_LOG(ERROR) << "Early stop requested for test " << env.test_name << " with failure exit code " << exit_code; } else { @@ -513,10 +507,11 @@ int UpdateCorpusDatabase(Environment env, << "\n\tTest binary: " << env.binary; const absl::Time start_time = absl::Now(); - ClearEarlyStopRequestAndSetStopTime(/*stop_time=*/start_time + time_limit); + stop_condition.ClearEarlyStopRequestAndSetStopTime(/*stop_time=*/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); + Fuzz(env, binary_info, pcs_file_path, callbacks_factory, stop_condition); record_fuzzing_time.Nudge(); record_fuzzing_time.Stop(); @@ -528,9 +523,9 @@ int UpdateCorpusDatabase(Environment env, (stats_dir / absl::StrCat("fuzzing_stats_", execution_stamp)).c_str())); } - if (EarlyStopRequested()) { - if (ExitCode() != EXIT_SUCCESS) { - exit_code = ExitCode(); + if (stop_condition.EarlyStopRequested()) { + if (stop_condition.ExitCode() != EXIT_SUCCESS) { + exit_code = stop_condition.ExitCode(); FUZZTEST_LOG(ERROR) << "Early stop requested for test " << env.test_name << " with failure exit code " << exit_code; } else { @@ -540,7 +535,8 @@ int UpdateCorpusDatabase(Environment env, return exit_code; } // The test time limit does not apply for the rest of the steps. - ClearEarlyStopRequestAndSetStopTime(/*stop_time=*/absl::InfiniteFuture()); + stop_condition.ClearEarlyStopRequestAndSetStopTime( + /*stop_time=*/absl::InfiniteFuture()); // TODO(xinhaoyuan): Have a separate flag to skip corpus updating instead // of checking whether workdir is specified or not. @@ -564,8 +560,8 @@ int UpdateCorpusDatabase(Environment env, return exit_code; } OrganizeCrashingInputs(regression_dir, fuzztest_db_path / "crashing", env, - callbacks_factory, crashes_by_signature, - crash_summary); + callbacks_factory, crashes_by_signature, crash_summary, + stop_condition); if (env.report_crash_summary) crash_summary.Report(&std::cerr); // Distill and store the coverage corpus. @@ -618,7 +614,8 @@ int ListCrashIds(const Environment& env) { } int ReplayCrash(const Environment& env, - CentipedeCallbacksFactory& callbacks_factory) { + 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()) @@ -646,7 +643,8 @@ int ReplayCrash(const Environment& env, crash_corpus_config, env.binary_name, env.binary_hash)); Environment run_crash_env = env; run_crash_env.load_shards_only = true; - int fuzz_result = Fuzz(run_crash_env, {}, "", callbacks_factory); + int fuzz_result = + 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 crashes_by_signature = @@ -694,9 +692,13 @@ int ExportCrash(const Environment& env) { } // namespace int CentipedeMain(const Environment& env, - CentipedeCallbacksFactory& callbacks_factory) { - ClearEarlyStopRequestAndSetStopTime(env.stop_at); - SetSignalHandlers(); + CentipedeCallbacksFactory& callbacks_factory, + StopCondition* stop_condition) { + StopCondition default_stop_condition; + if (stop_condition == nullptr) { + stop_condition = &default_stop_condition; + } + stop_condition->ClearEarlyStopRequestAndSetStopTime(env.stop_at); if (!env.corpus_to_files.empty()) { Centipede::CorpusToFiles(env, env.corpus_to_files); @@ -711,12 +713,12 @@ int CentipedeMain(const Environment& env, return EXIT_FAILURE; } - if (!env.for_each_blob.empty()) return ForEachBlob(env); + if (!env.for_each_blob.empty()) return ForEachBlob(env, *stop_condition); if (!env.minimize_crash_file_path.empty()) { ByteArray crashy_input; ReadFromLocalFile(env.minimize_crash_file_path, crashy_input); - return MinimizeCrash(crashy_input, env, callbacks_factory); + return MinimizeCrash(crashy_input, env, callbacks_factory, *stop_condition); } // Just export the corpus from a local dir and exit. @@ -758,7 +760,8 @@ int CentipedeMain(const Environment& env, absl::WebSafeBase64Unescape(env.fuzztest_configuration, &result)); return result; } - ScopedCentipedeCallbacks scoped_callbacks(callbacks_factory, env); + ScopedCentipedeCallbacks scoped_callbacks(callbacks_factory, env, + *stop_condition); return scoped_callbacks.callbacks()->GetSerializedTargetConfig(); }(); Environment updated_env = env; @@ -788,7 +791,7 @@ int CentipedeMain(const Environment& env, return ListCrashIds(updated_env); } if (env.replay_crash) { - return ReplayCrash(updated_env, callbacks_factory); + return ReplayCrash(updated_env, callbacks_factory, *stop_condition); } if (env.export_crash) { return ExportCrash(updated_env); @@ -801,7 +804,8 @@ int CentipedeMain(const Environment& env, FUZZTEST_CHECK(updated_env.fuzztest_time_limit_per_test >= absl::Seconds(1)) << "Time limit per fuzz test must be at least 1 second."; - return UpdateCorpusDatabase(updated_env, callbacks_factory); + return UpdateCorpusDatabase(updated_env, callbacks_factory, + *stop_condition); } } @@ -813,11 +817,12 @@ int CentipedeMain(const Environment& env, std::string pcs_file_path; BinaryInfo binary_info = PopulateBinaryInfoAndSavePCsIfNecessary( - env, callbacks_factory, pcs_file_path); + env, callbacks_factory, pcs_file_path, *stop_condition); if (env.analyze) return Analyze(env); - return Fuzz(env, binary_info, pcs_file_path, callbacks_factory); + return Fuzz(env, binary_info, pcs_file_path, callbacks_factory, + *stop_condition); // TODO: fniksic - Report the crash summary here if requested. What are the // binary identifier and the fuzz test name here? } diff --git a/centipede/centipede_interface.h b/centipede/centipede_interface.h index d6c5dfdd..818907dc 100644 --- a/centipede/centipede_interface.h +++ b/centipede/centipede_interface.h @@ -17,6 +17,7 @@ #include "./centipede/centipede_callbacks.h" #include "./centipede/environment.h" +#include "./centipede/stop.h" namespace fuzztest::internal { @@ -26,11 +27,13 @@ namespace fuzztest::internal { // InitGoogle(argv[0], &argc, &argv, /*remove_flags=*/true); // fuzztest::internal::Environment env; // reads FLAGS. // fuzztest::internal::DefaultCallbacksFactory -// callbacks_factory; return fuzztest::internal::CentipedeMain(env, -// callbacks_factory); +// callbacks_factory; +// return fuzztest::internal::CentipedeMain(env, callbacks_factory); // } -int CentipedeMain(const Environment &env, - CentipedeCallbacksFactory &callbacks_factory); + +int CentipedeMain(const Environment& env, + CentipedeCallbacksFactory& callbacks_factory, + StopCondition* stop_condition = nullptr); } // namespace fuzztest::internal diff --git a/centipede/centipede_main.cc b/centipede/centipede_main.cc index 8f6a7156..da7cb4c9 100644 --- a/centipede/centipede_main.cc +++ b/centipede/centipede_main.cc @@ -12,12 +12,35 @@ // See the License for the specific language governing permissions and // limitations under the License. +#include + +#include +#include + #include "absl/base/nullability.h" #include "./centipede/centipede_callbacks.h" #include "./centipede/centipede_default_callbacks.h" #include "./centipede/centipede_interface.h" #include "./centipede/config_file.h" #include "./centipede/environment_flags.h" +#include "./centipede/stop.h" + +namespace { +fuzztest::internal::StopCondition global_stop_condition; + +void SetSignalHandlers() { + struct sigaction sigact = {}; + sigact.sa_flags = SA_ONSTACK; + sigact.sa_handler = [](int received_signum) { + if (received_signum != SIGINT) return; + const char msg[] = "\n[!] Ctrl-C pressed: winding down\n"; + [[maybe_unused]] auto write_res = + write(STDERR_FILENO, msg, sizeof(msg) - 1); + global_stop_condition.RequestEarlyStop(EXIT_FAILURE); + }; + sigaction(SIGINT, &sigact, nullptr); +} +} // namespace int main(int argc, char** absl_nonnull argv) { const auto runtime_state = fuzztest::internal::InitCentipede(argc, argv); @@ -26,5 +49,7 @@ int main(int argc, char** absl_nonnull argv) { fuzztest::internal::DefaultCallbacksFactory< fuzztest::internal::CentipedeDefaultCallbacks> callbacks; - return CentipedeMain(env, callbacks); + SetSignalHandlers(); + int ret = CentipedeMain(env, callbacks, &global_stop_condition); + return ret; } diff --git a/centipede/centipede_test.cc b/centipede/centipede_test.cc index 0dd93566..e010a7f0 100644 --- a/centipede/centipede_test.cc +++ b/centipede/centipede_test.cc @@ -74,7 +74,8 @@ inline std::vector AsByteSpans(const std::vector& inputs) { // A mock for CentipedeCallbacks. class CentipedeMock : public CentipedeCallbacks { public: - CentipedeMock(const Environment &env) : CentipedeCallbacks(env) {} + CentipedeMock(const Environment& env) + : CentipedeCallbacks(env, mock_stop_condition_) {} // Doesn't execute anything // Sets `batch_result.results()` based on the values of `inputs`: // Collects various stats about the inputs, to be checked in tests. @@ -142,6 +143,9 @@ class CentipedeMock : public CentipedeCallbacks { size_t num_mutations_ = 0; size_t max_batch_size_ = 0; size_t min_batch_size_ = -1; + + private: + StopCondition mock_stop_condition_; }; TEST(Centipede, MockTest) { @@ -352,7 +356,8 @@ TEST(Centipede, InputFilter) { // Callbacks for MutateViaExternalBinary test. class MutateCallbacks : public CentipedeCallbacks { public: - explicit MutateCallbacks(const Environment &env) : CentipedeCallbacks(env) {} + explicit MutateCallbacks(const Environment& env) + : CentipedeCallbacks(env, mock_stop_condition_) {} // Will not be called. bool Execute(std::string_view binary, absl::Span inputs, BatchResult& batch_result) override { @@ -368,6 +373,9 @@ class MutateCallbacks : public CentipedeCallbacks { // Redeclare a protected member function as public so the tests can call it. using CentipedeCallbacks::MutateViaExternalBinary; + + private: + StopCondition mock_stop_condition_; }; // Maintains `TemporaryLocalDirPath()` during the lifetime. @@ -389,6 +397,8 @@ class CentipedeWithTemporaryLocalDir : public testing::Test { ~CentipedeWithTemporaryLocalDir() override { std::filesystem::remove_all(TemporaryLocalDirPath()); } + + StopCondition stop_condition; }; TEST_F(CentipedeWithTemporaryLocalDir, MutateViaExternalBinary) { @@ -495,7 +505,8 @@ TEST_F(CentipedeWithTemporaryLocalDir, MutateViaExternalBinary) { // A mock for MergeFromOtherCorpus test. class MergeMock : public CentipedeCallbacks { public: - explicit MergeMock(const Environment &env) : CentipedeCallbacks(env) {} + explicit MergeMock(const Environment& env) + : CentipedeCallbacks(env, mock_stop_condition_) {} // Doesn't execute anything. // All inputs are 1-byte long. @@ -526,6 +537,7 @@ class MergeMock : public CentipedeCallbacks { private: size_t number_of_mutations_ = 0; + StopCondition mock_stop_condition_; }; TEST(Centipede, MergeFromOtherCorpus) { @@ -578,8 +590,8 @@ TEST(Centipede, MergeFromOtherCorpus) { // A mock for FunctionFilter test. class FunctionFilterMock : public CentipedeCallbacks { public: - explicit FunctionFilterMock(const Environment &env) - : CentipedeCallbacks(env) { + explicit FunctionFilterMock(const Environment& env) + : CentipedeCallbacks(env, mock_stop_condition_) { std::vector seed_inputs; const size_t num_seeds_available = GetSeeds(/*num_seeds=*/1, seed_inputs); FUZZTEST_CHECK_EQ(num_seeds_available, 1) @@ -628,6 +640,7 @@ class FunctionFilterMock : public CentipedeCallbacks { private: size_t number_of_mutations_ = 0; + StopCondition mock_stop_condition_; }; // Runs a short fuzzing session with the provided `function_filter`. @@ -695,8 +708,9 @@ struct Crash { // A mock for ExtraBinaries test. class ExtraBinariesMock : public CentipedeCallbacks { public: - explicit ExtraBinariesMock(const Environment &env, std::vector crashes) - : CentipedeCallbacks(env), crashes_(std::move(crashes)) {} + explicit ExtraBinariesMock(const Environment& env, std::vector crashes) + : CentipedeCallbacks(env, mock_stop_condition_), + crashes_(std::move(crashes)) {} // Doesn't execute anything. // On certain combinations of {binary,input} returns false. @@ -733,6 +747,7 @@ class ExtraBinariesMock : public CentipedeCallbacks { private: size_t number_of_mutations_ = 0; std::vector crashes_; + StopCondition mock_stop_condition_; }; struct FileAndContents { @@ -811,9 +826,10 @@ TEST(Centipede, ExtraBinaries) { // A mock for UndetectedCrashingInput test. class UndetectedCrashingInputMock : public CentipedeCallbacks { public: - explicit UndetectedCrashingInputMock(const Environment &env, + explicit UndetectedCrashingInputMock(const Environment& env, size_t crashing_input_idx) - : CentipedeCallbacks{env}, crashing_input_idx_{crashing_input_idx} { + : CentipedeCallbacks{env, mock_stop_condition_}, + crashing_input_idx_{crashing_input_idx} { FUZZTEST_CHECK_LE(crashing_input_idx_, std::numeric_limits::max()); } @@ -876,6 +892,7 @@ class UndetectedCrashingInputMock : public CentipedeCallbacks { size_t num_inputs_triaged_ = 0; ByteArray crashing_input_ = {}; bool first_pass_ = true; + StopCondition mock_stop_condition_; }; // Test for preserving a crashing batch when 1-by-1 exec fails to reproduce. @@ -943,7 +960,7 @@ TEST_F(CentipedeWithTemporaryLocalDir, GetsSeedInputs) { Environment env; env.binary = GetDataDependencyFilepath("centipede/testing/seeded_fuzz_target"); - CentipedeDefaultCallbacks callbacks(env); + CentipedeDefaultCallbacks callbacks(env, stop_condition); std::vector seeds; EXPECT_EQ(callbacks.GetSeeds(10, seeds), 10); @@ -961,7 +978,7 @@ TEST_F(CentipedeWithTemporaryLocalDir, GetsSerializedTargetConfig) { Environment env; env.binary = GetDataDependencyFilepath("centipede/testing/fuzz_target_with_config"); - CentipedeDefaultCallbacks callbacks(env); + CentipedeDefaultCallbacks callbacks(env, stop_condition); const auto serialized_config = callbacks.GetSerializedTargetConfig(); ASSERT_TRUE(serialized_config.ok()); @@ -975,7 +992,7 @@ TEST_F(CentipedeWithTemporaryLocalDir, GetDataDependencyFilepath("centipede/testing/fuzz_target_with_config") .c_str(), " --simulate_failure"); - CentipedeDefaultCallbacks callbacks(env); + CentipedeDefaultCallbacks callbacks(env, stop_condition); const auto serialized_config = callbacks.GetSerializedTargetConfig(); EXPECT_FALSE(serialized_config.ok()); @@ -985,7 +1002,7 @@ TEST_F(CentipedeWithTemporaryLocalDir, CleansUpMetadataAfterStartup) { Environment env; env.binary = GetDataDependencyFilepath( "centipede/testing/expensive_startup_fuzz_target"); - CentipedeDefaultCallbacks callbacks(env); + CentipedeDefaultCallbacks callbacks(env, stop_condition); BatchResult batch_result; const std::vector inputs = {{0}}; @@ -1002,9 +1019,10 @@ TEST_F(CentipedeWithTemporaryLocalDir, CleansUpMetadataAfterStartup) { class FakeCentipedeCallbacksForThreadChecking : public CentipedeCallbacks { public: - FakeCentipedeCallbacksForThreadChecking(const Environment &env, + FakeCentipedeCallbacksForThreadChecking(const Environment& env, std::thread::id execute_thread_id) - : CentipedeCallbacks(env), execute_thread_id_(execute_thread_id) {} + : CentipedeCallbacks(env, mock_stop_condition_), + execute_thread_id_(execute_thread_id) {} bool Execute(std::string_view binary, absl::Span inputs, BatchResult& batch_result) override { @@ -1024,6 +1042,7 @@ class FakeCentipedeCallbacksForThreadChecking : public CentipedeCallbacks { private: std::thread::id execute_thread_id_; bool thread_check_passed_ = true; + StopCondition mock_stop_condition_; }; TEST(Centipede, RunsExecuteCallbackInTheCurrentThreadWhenFuzzingWithOneThread) { @@ -1046,7 +1065,7 @@ TEST_F(CentipedeWithTemporaryLocalDir, DetectsStackOverflow) { Environment env; env.binary = GetDataDependencyFilepath("centipede/testing/test_fuzz_target"); env.stack_limit_kb = 64; - CentipedeDefaultCallbacks callbacks(env); + CentipedeDefaultCallbacks callbacks(env, stop_condition); BatchResult batch_result; const std::vector inputs = {ByteArray{'s', 't', 'k'}}; @@ -1059,7 +1078,8 @@ TEST_F(CentipedeWithTemporaryLocalDir, DetectsStackOverflow) { class SetupFailureCallbacks : public CentipedeCallbacks { public: - using CentipedeCallbacks::CentipedeCallbacks; + explicit SetupFailureCallbacks(const Environment& env) + : CentipedeCallbacks(env, mock_stop_condition_) {} bool Execute(std::string_view binary, absl::Span inputs, BatchResult& batch_result) override { @@ -1079,6 +1099,7 @@ class SetupFailureCallbacks : public CentipedeCallbacks { private: int execute_count_ = 0; + StopCondition mock_stop_condition_; }; TEST(Centipede, ReturnsFailureOnSetupFailure) { @@ -1096,7 +1117,8 @@ TEST(Centipede, ReturnsFailureOnSetupFailure) { class SkippedTestCallbacks : public CentipedeCallbacks { public: - using CentipedeCallbacks::CentipedeCallbacks; + explicit SkippedTestCallbacks(const Environment& env) + : CentipedeCallbacks(env, mock_stop_condition_) {} bool Execute(std::string_view binary, absl::Span inputs, BatchResult& batch_result) override { @@ -1117,6 +1139,7 @@ class SkippedTestCallbacks : public CentipedeCallbacks { private: int execute_count_ = 0; + StopCondition mock_stop_condition_; }; TEST(Centipede, ReturnsSuccessOnSkippedTest) { @@ -1134,7 +1157,8 @@ TEST(Centipede, ReturnsSuccessOnSkippedTest) { class IgnoredFailureCallbacks : public CentipedeCallbacks { public: - using CentipedeCallbacks::CentipedeCallbacks; + explicit IgnoredFailureCallbacks(const Environment& env) + : CentipedeCallbacks(env, mock_stop_condition_) {} bool Execute(std::string_view binary, absl::Span inputs, BatchResult& batch_result) override { @@ -1155,6 +1179,7 @@ class IgnoredFailureCallbacks : public CentipedeCallbacks { private: int execute_count_ = 0; + StopCondition mock_stop_condition_; }; TEST(Centipede, KeepsRunningAndReturnsSuccessWithIgnoredFailures) { @@ -1176,7 +1201,7 @@ TEST(Centipede, KeepsRunningAndReturnsSuccessWithIgnoredFailures) { class CentipedeMockForInputReduction : public CentipedeCallbacks { public: CentipedeMockForInputReduction(const Environment& env) - : CentipedeCallbacks(env) {} + : CentipedeCallbacks(env, mock_stop_condition_) {} // Doesn't execute anything // Sets `batch_result.results()` based on the first 4 bits of `inputs`: bool Execute(std::string_view binary, absl::Span inputs, @@ -1199,6 +1224,9 @@ class CentipedeMockForInputReduction : public CentipedeCallbacks { } return num_seeds; } + + private: + StopCondition mock_stop_condition_; }; TEST(Centipede, DoesNotReduceInputWhenTheOptionIsUnset) { @@ -1247,7 +1275,7 @@ TEST_F(CentipedeWithTemporaryLocalDir, UsesProvidedCustomMutator) { Environment env; env.binary = GetDataDependencyFilepath( "centipede/testing/fuzz_target_with_custom_mutator"); - CentipedeDefaultCallbacks callbacks(env); + CentipedeDefaultCallbacks callbacks(env, stop_condition); const std::vector inputs = {{1}, {2}, {3}, {4}, {5}, {6}}; const std::vector mutants = callbacks.Mutate( @@ -1264,23 +1292,22 @@ TEST_F(CentipedeWithTemporaryLocalDir, FailsOnMisbehavingCustomMutator) { "centipede/testing/fuzz_target_with_custom_mutator") .c_str(), " --simulate_failure"); - CentipedeDefaultCallbacks callbacks(env); + StopCondition stop_condition; + CentipedeDefaultCallbacks callbacks(env, stop_condition); const std::vector inputs = {{1}, {2}, {3}, {4}, {5}, {6}}; - // Previous stop condition could interfere here. - ClearEarlyStopRequestAndSetStopTime(absl::InfiniteFuture()); EXPECT_THAT(callbacks.Mutate(GetMutationInputRefsFromDataInputs(inputs), inputs.size()), IsEmpty()); - EXPECT_TRUE(EarlyStopRequested()); - EXPECT_EQ(ExitCode(), EXIT_FAILURE); + EXPECT_TRUE(stop_condition.EarlyStopRequested()); + EXPECT_EQ(stop_condition.ExitCode(), EXIT_FAILURE); } TEST_F(CentipedeWithTemporaryLocalDir, FallsBackToBuiltInMutatorWhenCustomMutatorNotProvided) { Environment env; env.binary = GetDataDependencyFilepath("centipede/testing/abort_fuzz_target"); - CentipedeDefaultCallbacks callbacks(env); + CentipedeDefaultCallbacks callbacks(env, stop_condition); const std::vector inputs = {{1}, {2}, {3}, {4}, {5}, {6}}; const std::vector mutants = callbacks.Mutate( @@ -1295,14 +1322,14 @@ TEST_F(CentipedeWithTemporaryLocalDir, GetsSeedViaExternalBinaryStopsAfterStopTime) { Environment env; env.binary = "sleep 100"; - CentipedeDefaultCallbacks callbacks(env); + StopCondition stop_condition; + CentipedeDefaultCallbacks callbacks(env, stop_condition); const auto start = absl::Now(); - ClearEarlyStopRequestAndSetStopTime(start + absl::Seconds(3)); + stop_condition.ClearEarlyStopRequestAndSetStopTime(start + absl::Seconds(3)); std::vector seeds; callbacks.GetSeeds(/*num_seeds=*/1, seeds); // Give it some slack to stop in 5s. EXPECT_LE(absl::Now() - start, absl::Seconds(5)); - ClearEarlyStopRequestAndSetStopTime(absl::InfiniteFuture()); } TEST_F(CentipedeWithTemporaryLocalDir, HangingFuzzTargetExitsAfterTimeout) { @@ -1311,7 +1338,7 @@ TEST_F(CentipedeWithTemporaryLocalDir, HangingFuzzTargetExitsAfterTimeout) { GetDataDependencyFilepath("centipede/testing/hanging_fuzz_target"); BatchResult batch_result; const std::vector inputs = {{0}}; - CentipedeDefaultCallbacks callbacks(env); + CentipedeDefaultCallbacks callbacks(env, stop_condition); env.timeout_per_batch = 1; env.fork_server = false; @@ -1324,7 +1351,7 @@ TEST_F(CentipedeWithTemporaryLocalDir, HangingFuzzTargetExitsAfterTimeout) { TEST_F(CentipedeWithTemporaryLocalDir, ExecuteEndsAfterCustomFailure) { Environment env; env.binary = GetDataDependencyFilepath("centipede/testing/test_fuzz_target"); - CentipedeDefaultCallbacks callbacks(env); + CentipedeDefaultCallbacks callbacks(env, stop_condition); BatchResult result; std::vector inputs = { {'c', 'u', 's', 't', 'o', 'm'}, @@ -1344,12 +1371,12 @@ TEST_F(CentipedeWithTemporaryLocalDir, ToleratesAsyncFailureInMutation) { Environment env; env.binary = GetDataDependencyFilepath("centipede/testing/async_failing_target"); - CentipedeDefaultCallbacks callbacks(env); + StopCondition stop_condition; + CentipedeDefaultCallbacks callbacks(env, stop_condition); BatchResult result; std::vector inputs = { {'s', 'o', 'm', 'e'}, }; - ClearEarlyStopRequestAndSetStopTime(absl::InfiniteFuture()); EXPECT_TRUE(callbacks.Execute(env.binary, AsByteSpans(inputs), result)); // Match the error log to check for retrying mutation. EXPECT_DEATH( diff --git a/centipede/command.cc b/centipede/command.cc index 4c765985..4afab0ab 100644 --- a/centipede/command.cc +++ b/centipede/command.cc @@ -434,7 +434,8 @@ bool Command::ExecuteAsync() { return true; } -std::optional Command::Wait(absl::Time deadline) { +std::optional Command::Wait(absl::Time deadline, + StopCondition* stop_condition) { FUZZTEST_CHECK(is_executing()); int exit_code = EXIT_SUCCESS; @@ -506,7 +507,9 @@ std::optional Command::Wait(absl::Time deadline) { } else if (WIFSIGNALED(exit_code)) { const auto signal = WTERMSIG(exit_code); if (signal == SIGINT) { - RequestEarlyStop(EXIT_FAILURE); + if (stop_condition != nullptr) { + stop_condition->RequestEarlyStop(EXIT_FAILURE); + } // When the user kills Centipede via ^C, they are unlikely to be // interested in any of the subprocesses' outputs. Also, ^C terminates all // the subprocesses, including all the runners, so all their outputs would diff --git a/centipede/command.h b/centipede/command.h index f1272483..b574a84e 100644 --- a/centipede/command.h +++ b/centipede/command.h @@ -24,6 +24,7 @@ #include "absl/status/status.h" #include "absl/synchronization/mutex.h" #include "absl/time/time.h" +#include "./centipede/stop.h" namespace fuzztest::internal { @@ -90,9 +91,10 @@ class Command final { // Waits for the command execution and returns the exit status if the // execution finishes within `deadline`. Must be called only when the command - // is executing. execution or the execution times out. If interrupted, may - // call `RequestEarlyStop()` (see stop.h). - std::optional Wait(absl::Time deadline); + // is executing. If interrupted, may call `stop_condition->RequestEarlyStop()` + // (see stop.h). + std::optional Wait(absl::Time deadline, + StopCondition* stop_condition = nullptr); // Requests the command execution to stop by sending SIGTERM or SIGKILL (when // `force` is true). Must be called only when the command is executing. Note @@ -101,9 +103,9 @@ class Command final { void RequestStop(bool force = false); // Convenient method to execute synchronously. - int Execute() { + int Execute(StopCondition* stop_condition = nullptr) { if (!ExecuteAsync()) return EXIT_FAILURE; - return Wait(absl::InfiniteFuture()).value_or(EXIT_FAILURE); + return Wait(absl::InfiniteFuture(), stop_condition).value_or(EXIT_FAILURE); } // Attempts to start a fork server, returns true on success. diff --git a/centipede/command_test.cc b/centipede/command_test.cc index d7f2f583..524cedcd 100644 --- a/centipede/command_test.cc +++ b/centipede/command_test.cc @@ -58,20 +58,18 @@ TEST(CommandTest, Execute) { // Check for default exit code. Command echo{"echo"}; EXPECT_EQ(echo.Execute(), 0); - EXPECT_FALSE(ShouldStop()); // Check for exit code 7. Command exit7{"bash -c 'exit 7'"}; EXPECT_EQ(exit7.Execute(), 7); - EXPECT_FALSE(ShouldStop()); } TEST(CommandTest, HandlesInterruptedCommand) { + StopCondition stop_condition; Command self_sigint{"bash -c 'kill -SIGINT $$'"}; self_sigint.ExecuteAsync(); - self_sigint.Wait(absl::InfiniteFuture()); - EXPECT_TRUE(ShouldStop()); - ClearEarlyStopRequestAndSetStopTime(absl::InfiniteFuture()); + self_sigint.Wait(absl::InfiniteFuture(), &stop_condition); + EXPECT_TRUE(stop_condition.ShouldStop()); } TEST(CommandTest, InputFileWildCard) { diff --git a/centipede/crash_deduplication.cc b/centipede/crash_deduplication.cc index 545eb2dc..268c13ce 100644 --- a/centipede/crash_deduplication.cc +++ b/centipede/crash_deduplication.cc @@ -33,6 +33,7 @@ #include "./centipede/crash_summary.h" #include "./centipede/environment.h" #include "./centipede/runner_result.h" +#include "./centipede/stop.h" #include "./centipede/workdir.h" #include "./common/crashing_input_filename.h" #include "./common/defs.h" @@ -132,7 +133,7 @@ void OrganizeCrashingInputs( CentipedeCallbacksFactory& callbacks_factory, const absl::flat_hash_map& new_crashes_by_signature, - CrashSummary& crash_summary) { + CrashSummary& crash_summary, StopCondition& stop_condition) { FUZZTEST_CHECK_OK(RemoteMkdir(crashing_dir.c_str())); FUZZTEST_CHECK_OK(RemoteMkdir(regression_dir.c_str())); @@ -141,7 +142,8 @@ void OrganizeCrashingInputs( std::vector old_input_files = ValueOrDie(RemoteListFiles(crashing_dir.c_str(), /*recursively=*/false)); size_t crash_input_count = old_input_files.size(); - ScopedCentipedeCallbacks scoped_callbacks(callbacks_factory, env); + ScopedCentipedeCallbacks scoped_callbacks(callbacks_factory, env, + stop_condition); BatchResult batch_result; absl::flat_hash_map reproduced_crashes; diff --git a/centipede/crash_deduplication.h b/centipede/crash_deduplication.h index f5859eb1..936d12a7 100644 --- a/centipede/crash_deduplication.h +++ b/centipede/crash_deduplication.h @@ -85,7 +85,7 @@ void OrganizeCrashingInputs( CentipedeCallbacksFactory& callbacks_factory, const absl::flat_hash_map& new_crashes_by_signature, - CrashSummary& crash_summary); + CrashSummary& crash_summary, StopCondition& stop_condition); } // namespace fuzztest::internal diff --git a/centipede/crash_deduplication_test.cc b/centipede/crash_deduplication_test.cc index f8788716..2155b7f5 100644 --- a/centipede/crash_deduplication_test.cc +++ b/centipede/crash_deduplication_test.cc @@ -32,6 +32,7 @@ #include "./centipede/crash_summary.h" #include "./centipede/environment.h" #include "./centipede/runner_result.h" +#include "./centipede/stop.h" #include "./centipede/util.h" #include "./centipede/workdir.h" #include "./common/defs.h" @@ -167,7 +168,8 @@ class FakeCentipedeCallbacks : public CentipedeCallbacks { explicit FakeCentipedeCallbacks( const Environment& env, absl::flat_hash_map crashing_inputs) - : CentipedeCallbacks(env), crashing_inputs_(std::move(crashing_inputs)) {} + : CentipedeCallbacks(env, mock_stop_condition_), + crashing_inputs_(std::move(crashing_inputs)) {} bool Execute(std::string_view binary, absl::Span inputs, BatchResult& batch_result) override { @@ -185,6 +187,7 @@ class FakeCentipedeCallbacks : public CentipedeCallbacks { private: absl::flat_hash_map crashing_inputs_; + StopCondition mock_stop_condition_; }; struct FileAndContents { @@ -228,6 +231,7 @@ class OrganizeCrashingInputsTest : public ::testing::Test { } const Environment& env() const { return env_; } CrashSummary& crash_summary() { return crash_summary_; } + StopCondition& stop_condition() { return stop_condition_; } private: TempDir test_dir_; @@ -236,6 +240,7 @@ class OrganizeCrashingInputsTest : public ::testing::Test { std::filesystem::path new_crashes_dir_; Environment env_; CrashSummary crash_summary_{"binary_id", "fuzz_test"}; + StopCondition stop_condition_; }; TEST_F(OrganizeCrashingInputsTest, CreatesDirectoriesIfMissing) { @@ -246,7 +251,8 @@ TEST_F(OrganizeCrashingInputsTest, CreatesDirectoriesIfMissing) { NonOwningCallbacksFactory factory(callbacks); OrganizeCrashingInputs(regression_dir, crashing_dir, env(), factory, - /*new_crashes_by_signature=*/{}, crash_summary()); + /*new_crashes_by_signature=*/{}, crash_summary(), + stop_condition()); const std::filesystem::directory_entry crashing_dir_entry{crashing_dir}; const std::filesystem::directory_entry regression_dir_entry{regression_dir}; @@ -263,7 +269,8 @@ TEST_F(OrganizeCrashingInputsTest, RenamesOldStyleCrashFileToNewStyle) { NonOwningCallbacksFactory factory(callbacks); OrganizeCrashingInputs(regression_dir(), crashing_dir(), env(), factory, - /*new_crashes_by_signature=*/{}, crash_summary()); + /*new_crashes_by_signature=*/{}, crash_summary(), + stop_condition()); std::string crash_report; crash_summary().Report(&crash_report); @@ -285,7 +292,8 @@ TEST_F(OrganizeCrashingInputsTest, KeepsNewStyleCrashFileIfSignatureUnchanged) { NonOwningCallbacksFactory factory(callbacks); OrganizeCrashingInputs(regression_dir(), crashing_dir(), env(), factory, - /*new_crashes_by_signature=*/{}, crash_summary()); + /*new_crashes_by_signature=*/{}, crash_summary(), + stop_condition()); std::string crash_report; crash_summary().Report(&crash_report); @@ -307,7 +315,8 @@ TEST_F(OrganizeCrashingInputsTest, UpdatesCrashSignatureInFileNameIfChanged) { NonOwningCallbacksFactory factory(callbacks); OrganizeCrashingInputs(regression_dir(), crashing_dir(), env(), factory, - /*new_crashes_by_signature=*/{}, crash_summary()); + /*new_crashes_by_signature=*/{}, crash_summary(), + stop_condition()); std::string crash_report; crash_summary().Report(&crash_report); @@ -340,7 +349,8 @@ TEST_F(OrganizeCrashingInputsTest, NonOwningCallbacksFactory factory(callbacks); OrganizeCrashingInputs(regression_dir(), crashing_dir(), env(), factory, - /*new_crashes_by_signature=*/{}, crash_summary()); + /*new_crashes_by_signature=*/{}, crash_summary(), + stop_condition()); EXPECT_GT(std::filesystem::last_write_time(reproducible_input_path), reproducible_mtime_before); @@ -359,7 +369,8 @@ TEST_F(OrganizeCrashingInputsTest, NonOwningCallbacksFactory factory(callbacks); OrganizeCrashingInputs(regression_dir(), crashing_dir(), env(), factory, - /*new_crashes_by_signature=*/{}, crash_summary()); + /*new_crashes_by_signature=*/{}, crash_summary(), + stop_condition()); std::string crash_report; crash_summary().Report(&crash_report); @@ -391,7 +402,8 @@ TEST_F(OrganizeCrashingInputsTest, KeepsFlakyCrashAndUpdatesModificationTime) { new_crashes_by_signature["csig"] = {"isig", "desc", new_input_path}; OrganizeCrashingInputs(regression_dir(), crashing_dir(), env(), factory, - new_crashes_by_signature, crash_summary()); + new_crashes_by_signature, crash_summary(), + stop_condition()); std::string crash_report; crash_summary().Report(&crash_report); @@ -413,7 +425,8 @@ TEST_F(OrganizeCrashingInputsTest, NonOwningCallbacksFactory factory(callbacks); OrganizeCrashingInputs(regression_dir(), crashing_dir(), env(), factory, - /*new_crashes_by_signature=*/{}, crash_summary()); + /*new_crashes_by_signature=*/{}, crash_summary(), + stop_condition()); std::string crash_report; crash_summary().Report(&crash_report); @@ -434,7 +447,8 @@ TEST_F(OrganizeCrashingInputsTest, NonOwningCallbacksFactory factory(callbacks); OrganizeCrashingInputs(regression_dir(), crashing_dir(), env(), factory, - /*new_crashes_by_signature=*/{}, crash_summary()); + /*new_crashes_by_signature=*/{}, crash_summary(), + stop_condition()); std::string crash_report; crash_summary().Report(&crash_report); @@ -452,7 +466,8 @@ TEST_F(OrganizeCrashingInputsTest, NonOwningCallbacksFactory factory(callbacks); OrganizeCrashingInputs(regression_dir(), crashing_dir(), env(), factory, - /*new_crashes_by_signature=*/{}, crash_summary()); + /*new_crashes_by_signature=*/{}, crash_summary(), + stop_condition()); std::string crash_report; crash_summary().Report(&crash_report); @@ -474,7 +489,8 @@ TEST_F(OrganizeCrashingInputsTest, new_crashes_by_signature["csig"] = {"isig2", "desc2", input2_path}; OrganizeCrashingInputs(regression_dir(), crashing_dir(), env(), factory, - new_crashes_by_signature, crash_summary()); + new_crashes_by_signature, crash_summary(), + stop_condition()); std::string crash_report; crash_summary().Report(&crash_report); @@ -499,7 +515,8 @@ TEST_F(OrganizeCrashingInputsTest, NonOwningCallbacksFactory factory(callbacks); OrganizeCrashingInputs(regression_dir(), crashing_dir(), env(), factory, - /*new_crashes_by_signature=*/{}, crash_summary()); + /*new_crashes_by_signature=*/{}, crash_summary(), + stop_condition()); std::string crash_report; crash_summary().Report(&crash_report); @@ -525,7 +542,8 @@ TEST_F(OrganizeCrashingInputsTest, StoresNewCrashWithUniqueCrashSignature) { new_crashes_by_signature["csig"] = {"isig", "desc", input_path}; OrganizeCrashingInputs(regression_dir(), crashing_dir(), env(), factory, - new_crashes_by_signature, crash_summary()); + new_crashes_by_signature, crash_summary(), + stop_condition()); std::string crash_report; crash_summary().Report(&crash_report); @@ -554,7 +572,8 @@ TEST_F(OrganizeCrashingInputsTest, new_crashes_by_signature["csig"] = {"isig2", "desc2", input2_path}; OrganizeCrashingInputs(regression_dir(), crashing_dir(), env(), factory, - new_crashes_by_signature, crash_summary()); + new_crashes_by_signature, crash_summary(), + stop_condition()); std::string crash_report; crash_summary().Report(&crash_report); @@ -575,7 +594,8 @@ TEST_F(OrganizeCrashingInputsTest, DoesNotProcessInputsInRegressionDir) { NonOwningCallbacksFactory factory(callbacks); OrganizeCrashingInputs(regression_dir(), crashing_dir(), env(), factory, - /*new_crashes_by_signature=*/{}, crash_summary()); + /*new_crashes_by_signature=*/{}, crash_summary(), + stop_condition()); std::string crash_report; crash_summary().Report(&crash_report); @@ -614,7 +634,8 @@ TEST_F(OrganizeCrashingInputsTest, AddsNewCrashesUpToFileLimit) { NonOwningCallbacksFactory factory(callbacks); OrganizeCrashingInputs(regression_dir(), crashing_dir(), env(), factory, - new_crashes_by_signature, crash_summary()); + new_crashes_by_signature, crash_summary(), + stop_condition()); std::string crash_report; crash_summary().Report(&crash_report); @@ -670,7 +691,8 @@ TEST_F(OrganizeCrashingInputsTest, ReplacesIrreproducibleCrashAtFileLimit) { NonOwningCallbacksFactory factory(callbacks); OrganizeCrashingInputs(regression_dir(), crashing_dir(), env(), factory, - new_crashes_by_signature, crash_summary()); + new_crashes_by_signature, crash_summary(), + stop_condition()); std::string crash_report; crash_summary().Report(&crash_report); diff --git a/centipede/minimize_crash.cc b/centipede/minimize_crash.cc index 245846cc..48ca00ce 100644 --- a/centipede/minimize_crash.cc +++ b/centipede/minimize_crash.cc @@ -85,10 +85,12 @@ struct MinimizerWorkQueue { }; // Performs a minimization loop in one thread. -static void MinimizeCrash(const Environment &env, - CentipedeCallbacksFactory &callbacks_factory, - MinimizerWorkQueue &queue) { - ScopedCentipedeCallbacks scoped_callback(callbacks_factory, env); +static void MinimizeCrash(const Environment& env, + CentipedeCallbacksFactory& callbacks_factory, + MinimizerWorkQueue& queue, + StopCondition& stop_condition) { + ScopedCentipedeCallbacks scoped_callback(callbacks_factory, env, + stop_condition); auto callbacks = scoped_callback.callbacks(); BatchResult batch_result; @@ -96,7 +98,7 @@ static void MinimizeCrash(const Environment &env, for (size_t i = 0; i < num_batches; ++i) { FUZZTEST_LOG_EVERY_POW_2(INFO) << "[" << i << "] Minimizing... Interrupt to stop"; - if (ShouldStop()) break; + if (stop_condition.ShouldStop()) break; // Get up to kMaxNumCrashersToGet most recent crashers. We don't want just // the most recent crasher to avoid being stuck in local minimum. constexpr size_t kMaxNumCrashersToGet = 20; @@ -133,9 +135,11 @@ static void MinimizeCrash(const Environment &env, } } -int MinimizeCrash(ByteSpan crashy_input, const Environment &env, - CentipedeCallbacksFactory &callbacks_factory) { - ScopedCentipedeCallbacks scoped_callback(callbacks_factory, env); +int MinimizeCrash(ByteSpan crashy_input, const Environment& env, + CentipedeCallbacksFactory& callbacks_factory, + StopCondition& stop_condition) { + ScopedCentipedeCallbacks scoped_callback(callbacks_factory, env, + stop_condition); auto callbacks = scoped_callback.callbacks(); FUZZTEST_LOG(INFO) << "MinimizeCrash: trying the original crashy input"; @@ -156,8 +160,8 @@ int MinimizeCrash(ByteSpan crashy_input, const Environment &env, { ThreadPool threads{static_cast(env.num_threads)}; for (size_t i = 0; i < env.num_threads; ++i) { - threads.Schedule([&env, &callbacks_factory, &queue]() { - MinimizeCrash(env, callbacks_factory, queue); + threads.Schedule([&env, &callbacks_factory, &queue, &stop_condition]() { + MinimizeCrash(env, callbacks_factory, queue, stop_condition); }); } } // The threads join here. diff --git a/centipede/minimize_crash.h b/centipede/minimize_crash.h index 5677565d..13dffd03 100644 --- a/centipede/minimize_crash.h +++ b/centipede/minimize_crash.h @@ -21,6 +21,8 @@ namespace fuzztest::internal { +class StopCondition; + // Tries to minimize `crashy_input`. // Uses `callbacks_factory` to create `env.num_threads` workers. // Returns EXIT_SUCCESS if at least one smaller crasher was found, @@ -28,8 +30,9 @@ namespace fuzztest::internal { // Also returns EXIT_FAILURE if the original input didn't crash. // Stores the newly found crashy inputs in // `WorkDir{env}.CrashReproducerDirPath()`. -int MinimizeCrash(ByteSpan crashy_input, const Environment &env, - CentipedeCallbacksFactory &callbacks_factory); +int MinimizeCrash(ByteSpan crashy_input, const Environment& env, + CentipedeCallbacksFactory& callbacks_factory, + StopCondition& stop_condition); } // namespace fuzztest::internal diff --git a/centipede/minimize_crash_test.cc b/centipede/minimize_crash_test.cc index 655c222c..2d2df094 100644 --- a/centipede/minimize_crash_test.cc +++ b/centipede/minimize_crash_test.cc @@ -27,6 +27,7 @@ #include "./centipede/centipede_callbacks.h" #include "./centipede/environment.h" #include "./centipede/runner_result.h" +#include "./centipede/stop.h" #include "./centipede/util.h" #include "./centipede/workdir.h" #include "./common/defs.h" @@ -38,7 +39,8 @@ namespace { // A mock for CentipedeCallbacks. class MinimizerMock : public CentipedeCallbacks { public: - MinimizerMock(const Environment &env) : CentipedeCallbacks(env) {} + MinimizerMock(const Environment& env) + : CentipedeCallbacks(env, mock_stop_condition_) {} // Runs FuzzMe() on every input, imitates failure if FuzzMe() returns true. bool Execute(std::string_view binary, absl::Span inputs, @@ -65,12 +67,15 @@ class MinimizerMock : public CentipedeCallbacks { } return false; } + + StopCondition mock_stop_condition_; }; // Factory that creates/destroys MinimizerMock. class MinimizerMockFactory : public CentipedeCallbacksFactory { public: - CentipedeCallbacks *absl_nonnull create(const Environment &env) override { + CentipedeCallbacks* absl_nonnull create( + const Environment& env, StopCondition& stop_condition) override { return new MinimizerMock(env); } void destroy(CentipedeCallbacks *cb) override { delete cb; } @@ -83,20 +88,24 @@ TEST(MinimizeTest, MinimizeTest) { env.num_runs = 100000; const WorkDir wd{env}; MinimizerMockFactory factory; + StopCondition stop_condition; // Test with a non-crashy input. - EXPECT_EQ(MinimizeCrash({1, 2, 3}, env, factory), EXIT_FAILURE); + EXPECT_EQ(MinimizeCrash({1, 2, 3}, env, factory, stop_condition), + EXIT_FAILURE); ByteArray expected_minimized = {'f', 'u', 'z'}; // Test with a crashy input that can't be minimized further. - EXPECT_EQ(MinimizeCrash(expected_minimized, env, factory), EXIT_FAILURE); + EXPECT_EQ(MinimizeCrash(expected_minimized, env, factory, stop_condition), + EXIT_FAILURE); // Test the actual minimization. ByteArray original_crasher = {'f', '.', '.', '.', '.', '.', '.', '.', '.', '.', '.', 'u', '.', '.', '.', '.', '.', '.', '.', '.', '.', '.', 'z'}; - EXPECT_EQ(MinimizeCrash(original_crasher, env, factory), EXIT_SUCCESS); + EXPECT_EQ(MinimizeCrash(original_crasher, env, factory, stop_condition), + EXIT_SUCCESS); // Collect the new crashers from the crasher dir. std::vector crashers; for (auto const &dir_entry : std::filesystem::directory_iterator{ diff --git a/centipede/stop.cc b/centipede/stop.cc index 6a76c17a..e230e2d7 100644 --- a/centipede/stop.cc +++ b/centipede/stop.cc @@ -21,35 +21,30 @@ #include "absl/time/time.h" namespace fuzztest::internal { -namespace { -struct EarlyStop { - int exit_code = EXIT_SUCCESS; - bool is_requested = false; -}; -std::atomic early_stop; +// StopCondition implementation -absl::Time stop_time = absl::InfiniteFuture(); - -} // namespace - -bool EarlyStopRequested() { - return early_stop.load(std::memory_order_acquire).is_requested; +bool StopCondition::EarlyStopRequested() const { + return early_stop_.load(std::memory_order_acquire).is_requested; } -void ClearEarlyStopRequestAndSetStopTime(absl::Time stop_time) { - early_stop.store({}, std::memory_order_release); - ::fuzztest::internal::stop_time = stop_time; +void StopCondition::ClearEarlyStopRequestAndSetStopTime(absl::Time stop_time) { + early_stop_.store({}, std::memory_order_release); + stop_time_ = stop_time; } -void RequestEarlyStop(int exit_code) { - early_stop.store({exit_code, true}, std::memory_order_release); +void StopCondition::RequestEarlyStop(int exit_code) { + early_stop_.store({exit_code, true}, std::memory_order_release); } -absl::Time GetStopTime() { return stop_time; } +absl::Time StopCondition::GetStopTime() const { return stop_time_; } -bool ShouldStop() { return EarlyStopRequested() || stop_time < absl::Now(); } +bool StopCondition::ShouldStop() const { + return EarlyStopRequested() || stop_time_ < absl::Now(); +} -int ExitCode() { return early_stop.load(std::memory_order_acquire).exit_code; } +int StopCondition::ExitCode() const { + return early_stop_.load(std::memory_order_acquire).exit_code; +} } // namespace fuzztest::internal diff --git a/centipede/stop.h b/centipede/stop.h index d3e255dd..cdb0e5a2 100644 --- a/centipede/stop.h +++ b/centipede/stop.h @@ -15,51 +15,74 @@ #ifndef THIRD_PARTY_CENTIPEDE_STOP_H_ #define THIRD_PARTY_CENTIPEDE_STOP_H_ +#include +#include + #include "absl/time/time.h" namespace fuzztest::internal { -// Clears the request to stop early and sets the stop time. -// -// REQUIRES: Must be called before starting concurrent threads that may invoke -// the functions defined in this header. In particular, calling this function -// concurrently with `ShouldStop()` is not thread-safe. -void ClearEarlyStopRequestAndSetStopTime(absl::Time stop_time); +// Encapsulates the stop condition state for Centipede. +class StopCondition { + public: + StopCondition() = default; -// Requests that Centipede soon stops whatever it is doing (fuzzing, minimizing -// reproducer, etc.), with `exit_code` indicating success (zero) or failure -// (non-zero). -// -// ENSURES: Thread-safe and safe to call from signal handlers. -void RequestEarlyStop(int exit_code); + // Non-copyable and non-movable. + StopCondition(const StopCondition&) = delete; + StopCondition& operator=(const StopCondition&) = delete; + StopCondition(StopCondition&&) = delete; + StopCondition& operator=(StopCondition&&) = delete; -// Returns whether `RequestEarlyStop()` was called or not since the most recent -// call to `ClearEarlyStopRequestAndSetStopTime()` (if any). -// -// ENSURES: Thread-safe. -bool EarlyStopRequested(); + // Clears the request to stop early and sets the stop time. + // + // REQUIRES: Must be called before starting concurrent threads that may invoke + // the functions defined in this class. In particular, calling this function + // concurrently with `ShouldStop()` is not thread-safe. + void ClearEarlyStopRequestAndSetStopTime(absl::Time stop_time); -// Returns true iff it is time to stop, either because the stopping time has -// been reached or `RequestEarlyStop()` was called since the most recent call to -// `ClearEarlyStopRequestAndSetStopTime()` (if any). -// -// ENSURES: Thread-safe. -bool ShouldStop(); + // Requests that Centipede soon stops whatever it is doing (fuzzing, + // minimizing reproducer, etc.), with `exit_code` indicating success (zero) or + // failure (non-zero). + // + // ENSURES: Thread-safe and safe to call from signal handlers. + void RequestEarlyStop(int exit_code); -// Returns the stop time set from the recent -// `ClearEarlyStopRequestAndSetStopTime()`, or `absl::InfiniteFuture()` it was -// not set. -// -// ENSURES: Thread-safe. -absl::Time GetStopTime(); + // Returns whether `RequestEarlyStop()` was called or not since the most + // recent call to `ClearEarlyStopRequestAndSetStopTime()` (if any). + // + // ENSURES: Thread-safe. + bool EarlyStopRequested() const; -// Returns the value most recently passed to `RequestEarlyStop()` or 0 if -// `RequestEarlyStop()` was not called since the most recent call to -// `ClearEarlyStopRequestAndSetStopTime()` (if any). -// -// ENSURES: Thread-safe. -int ExitCode(); + // Returns true iff it is time to stop, either because the stopping time has + // been reached or `RequestEarlyStop()` was called since the most recent call + // to `ClearEarlyStopRequestAndSetStopTime()` (if any). + // + // ENSURES: Thread-safe. + bool ShouldStop() const; + + // Returns the stop time set from the recent + // `ClearEarlyStopRequestAndSetStopTime()`, or `absl::InfiniteFuture()` if it + // was not set. + // + // ENSURES: Thread-safe. + absl::Time GetStopTime() const; + + // Returns the value most recently passed to `RequestEarlyStop()` or 0 if + // `RequestEarlyStop()` was not called since the most recent call to + // `ClearEarlyStopRequestAndSetStopTime()` (if any). + // + // ENSURES: Thread-safe. + int ExitCode() const; + private: + struct EarlyStop { + int exit_code = EXIT_SUCCESS; + bool is_requested = false; + }; + static_assert(std::atomic::is_always_lock_free); + std::atomic early_stop_{EarlyStop{}}; + absl::Time stop_time_ = absl::InfiniteFuture(); +}; } // namespace fuzztest::internal #endif // THIRD_PARTY_CENTIPEDE_STOP_H_ diff --git a/centipede/test_coverage_util.h b/centipede/test_coverage_util.h index a3385240..a51064f2 100644 --- a/centipede/test_coverage_util.h +++ b/centipede/test_coverage_util.h @@ -28,6 +28,7 @@ #include "./centipede/feature.h" #include "./centipede/mutation_data.h" #include "./centipede/runner_result.h" +#include "./centipede/stop.h" #include "./common/defs.h" namespace fuzztest::internal { // Runs all `inputs`, returns FeatureVec for every input. @@ -43,7 +44,8 @@ std::vector RunInputsAndCollectCoverage( // A simple CentipedeCallbacks derivative. class TestCallbacks : public CentipedeCallbacks { public: - explicit TestCallbacks(const Environment &env) : CentipedeCallbacks(env) {} + explicit TestCallbacks(const Environment& env) + : CentipedeCallbacks(env, mock_stop_condition_) {} bool Execute(std::string_view binary, absl::Span inputs, BatchResult& batch_result) override { int result = @@ -55,6 +57,9 @@ class TestCallbacks : public CentipedeCallbacks { size_t num_mutants) override { return {}; } + + private: + StopCondition mock_stop_condition_; }; } // namespace fuzztest::internal diff --git a/fuzztest/internal/centipede_adaptor.cc b/fuzztest/internal/centipede_adaptor.cc index aafc9e0d..d46c61e3 100644 --- a/fuzztest/internal/centipede_adaptor.cc +++ b/fuzztest/internal/centipede_adaptor.cc @@ -351,6 +351,8 @@ fuzztest::internal::Environment CreateCentipedeEnvironmentFromConfiguration( return env; } +static StopCondition global_stop_condition; + void InstallCentipedeTerminationHandler() { [[maybe_unused]] static bool install_once = [] { for (int signum : {SIGTERM, SIGHUP}) { @@ -358,7 +360,7 @@ void InstallCentipedeTerminationHandler() { sigemptyset(&new_sigact.sa_mask); new_sigact.sa_handler = [](int signum) { Runtime::instance().SetTerminationRequested(); - RequestEarlyStop(EXIT_SUCCESS); + global_stop_condition.RequestEarlyStop(EXIT_SUCCESS); const int fd = GetStderrFdDup() != -1 ? GetStderrFdDup() : STDERR_FILENO; if (signum == SIGTERM) { @@ -428,9 +430,11 @@ int RunCentipede(const Environment& env, << "Termination status must be Exited if not Signaled"; return static_cast(std::get(status.Status())); } + global_stop_condition.ClearEarlyStopRequestAndSetStopTime( + absl::InfiniteFuture()); static absl::NoDestructor> factory; - return CentipedeMain(env, *factory); + return CentipedeMain(env, *factory, &global_stop_condition); } } // namespace @@ -1079,7 +1083,8 @@ extern "C" const char* CentipedeGetRunnerFlags() { // Set the runner flags according to the FuzzTest default environment. const auto env = fuzztest::internal::CreateDefaultCentipedeEnvironment(); - CentipedeCallbacksForRunnerFlagsExtraction callbacks(env); + CentipedeCallbacksForRunnerFlagsExtraction callbacks( + env, fuzztest::internal::global_stop_condition); const std::string runner_flags = callbacks.GetRunnerFlagsContent(); ABSL_VLOG(1) << "[.] Centipede runner flags: " << runner_flags; return strdup(runner_flags.c_str());