From 327949ab8193ef2af4b532b8fbe3396b0eb5f754 Mon Sep 17 00:00:00 2001 From: Xinhao Yuan Date: Tue, 23 Jun 2026 10:38:52 -0700 Subject: [PATCH] Localize the stop condition to make it more library-friendly. Otherwise, when invoking the library interface, one invocation that triggers an early stop can affect other invocations. Also currently the library call installs signal handlers, which is global and can conflict with existing handlers. Moved the signal handling from the library to the binary entry code. The main library entry CentipedeMain takes an optional StopCondition to allow early stop to be requested externally (from the signal handlers). PiperOrigin-RevId: 936759616 --- centipede/BUILD | 7 +- centipede/centipede.cc | 38 +++---- centipede/centipede.h | 8 +- centipede/centipede_callbacks.cc | 7 +- centipede/centipede_callbacks.h | 24 +++-- centipede/centipede_callbacks_test.cc | 19 +++- centipede/centipede_default_callbacks.cc | 9 +- centipede/centipede_default_callbacks.h | 4 +- centipede/centipede_interface.cc | 123 ++++++++++++----------- centipede/centipede_interface.h | 11 +- centipede/centipede_main.cc | 27 ++++- centipede/centipede_test.cc | 95 ++++++++++------- centipede/command.cc | 7 +- centipede/command.h | 12 ++- centipede/command_test.cc | 8 +- centipede/crash_deduplication.cc | 6 +- centipede/crash_deduplication.h | 2 +- centipede/crash_deduplication_test.cc | 58 +++++++---- centipede/minimize_crash.cc | 24 +++-- centipede/minimize_crash.h | 7 +- centipede/minimize_crash_test.cc | 19 +++- centipede/stop.cc | 35 +++---- centipede/stop.h | 93 ++++++++++------- centipede/test_coverage_util.h | 7 +- fuzztest/internal/centipede_adaptor.cc | 11 +- 25 files changed, 410 insertions(+), 251 deletions(-) diff --git a/centipede/BUILD b/centipede/BUILD index 37d2a1d35..69d2f1b08 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 c8d09b049..e2a0ee336 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 18fa83d84..0ce733908 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 e5603697e..b03b48aad 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 a4fb2bb4f..38d6bc35c 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 40a5fc1b4..317641534 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 3f0717456..d8b01a35a 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 02fff74f4..d5e7b75fc 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 e07ca4b66..c519be111 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 d6c5dfdd9..818907dcf 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 8f6a71564..da7cb4c94 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 0dd935663..e010a7f0a 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 4c765985d..4afab0abb 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 f1272483b..b574a84ee 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 d7f2f583e..524cedcd2 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 545eb2dcf..268c13ce3 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 f5859eb1c..936d12a7d 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 f8788716a..2155b7f54 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 245846cc4..48ca00ce7 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 5677565d1..13dffd03c 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 655c222cd..2d2df0943 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 6a76c17a2..e230e2d75 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 d3e255ddb..cdb0e5a27 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 a33852407..a51064f2c 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 aafc9e0d6..d46c61e30 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());