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: 940460449
diff --git a/centipede/BUILD b/centipede/BUILD
index efab7bf..3392294 100644
--- a/centipede/BUILD
+++ b/centipede/BUILD
@@ -44,8 +44,8 @@
         ":centipede_interface",
         ":config_file",
         ":environment_flags",
+        ":stop",
         "@abseil-cpp//absl/base:nullability",
-        "@abseil-cpp//absl/log:flags",
     ],
 )
 
@@ -898,6 +898,7 @@
         ":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 @@
         ":feature",
         ":mutation_data",
         ":runner_result",
+        ":stop",
         ":util",
         "@abseil-cpp//absl/types:span",
         "@com_google_fuzztest//common:defs",
@@ -1327,6 +1329,7 @@
         ":centipede_callbacks",
         ":environment",
         ":runner_result",
+        ":stop",
         "@abseil-cpp//absl/types:span",
         "@com_google_fuzztest//common:defs",
         "@googletest//:gtest_main",
@@ -1502,6 +1505,7 @@
         ":environment",
         ":minimize_crash",
         ":runner_result",
+        ":stop",
         ":util",
         ":workdir",
         "@abseil-cpp//absl/base:nullability",
@@ -1919,6 +1923,7 @@
         ":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 c8d09b0..e2a0ee3 100644
--- a/centipede/centipede.cc
+++ b/centipede/centipede.cc
@@ -115,8 +115,10 @@
 
 Centipede::Centipede(const Environment& env, CentipedeCallbacks& user_callbacks,
                      const BinaryInfo& binary_info,
-                     CoverageLogger& coverage_logger, std::atomic<Stats>& stats)
+                     CoverageLogger& coverage_logger, std::atomic<Stats>& stats,
+                     StopCondition& stop_condition)
     : env_(env),
+      stop_condition_(stop_condition),
       user_callbacks_(user_callbacks),
       rng_(env_.seed),
       // TODO(kcc): [impl] find a better way to compute frequency_threshold.
@@ -379,7 +381,7 @@
 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 @@
                        << 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 @@
   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 @@
   std::vector<ByteArray> 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 @@
   // 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 @@
   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 @@
 
     std::vector<Mutant> mutants =
         user_callbacks_.Mutate(mutation_inputs, batch_size);
-    if (ShouldStop()) break;
+    if (stop_condition_.ShouldStop()) break;
     new_runs += mutants.size();
 
     std::vector<MutantRef> mutant_refs;
@@ -1015,7 +1017,7 @@
   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 @@
     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 @@
       << 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 @@
     }
   }
 
-  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 18fa83d..0ce7339 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 @@
 // The main fuzzing class.
 class Centipede {
  public:
-  Centipede(const Environment &env, CentipedeCallbacks &user_callbacks,
-            const BinaryInfo &binary_info, CoverageLogger &coverage_logger,
-            std::atomic<Stats> &stats);
+  Centipede(const Environment& env, CentipedeCallbacks& user_callbacks,
+            const BinaryInfo& binary_info, CoverageLogger& coverage_logger,
+            std::atomic<Stats>& stats, StopCondition& stop_condition);
   virtual ~Centipede() = default;
 
   // Non-copyable and non-movable.
@@ -175,6 +176,7 @@
   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 e560369..b03b48a 100644
--- a/centipede/centipede_callbacks.cc
+++ b/centipede/centipede_callbacks.cc
@@ -469,7 +469,7 @@
           ? 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 @@
       }
       return false;
     }
-    const std::optional<int> ret = cmd.Wait(deadline);
+    const std::optional<int> ret = cmd.Wait(deadline, &stop_condition_);
     if (!ret.has_value()) return true;
     exit_code = *ret;
     return false;
@@ -660,7 +660,8 @@
                           << 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 a4fb2bb..38d6bc3 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 @@
 // 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 @@
   void PrintExecutionLog() const;
 
   const Environment &env_;
+  StopCondition& stop_condition_;
   ByteArrayMutator byte_array_mutator_;
   FuzzTestMutator fuzztest_mutator_;
 
@@ -216,7 +218,8 @@
 // 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 @@
 template <typename Type>
 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 @@
  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 @@
 // 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 40a5fc1..8d02e73 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 @@
 
 class FakeCallbacks : public CentipedeCallbacks {
  public:
-  explicit FakeCallbacks(const Environment& env) : CentipedeCallbacks(env) {}
+  explicit FakeCallbacks(const Environment& env)
+      : CentipedeCallbacks(env, internal_stop_condition_) {}
   bool Execute(std::string_view binary, absl::Span<const ByteSpan> inputs,
                BatchResult& batch_result) override {
     return true;
   }
+
+ private:
+  StopCondition internal_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 @@
   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 3f07174..d8b01a3 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 @@
                "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 @@
       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 02fff74..d5e7b75 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 @@
 // 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<ByteArray> &seeds) override;
   absl::StatusOr<std::string> GetSerializedTargetConfig() override;
   bool Execute(std::string_view binary, absl::Span<const ByteSpan> inputs,
diff --git a/centipede/centipede_interface.cc b/centipede/centipede_interface.cc
index e07ca4b..c519be1 100644
--- a/centipede/centipede_interface.cc
+++ b/centipede/centipede_interface.cc
@@ -79,23 +79,10 @@
 // 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 @@
       // 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 @@
 
 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 @@
 
 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<Environment> envs =
@@ -225,20 +214,22 @@
         });
   }
 
-  auto fuzzing_worker =
-      [&env, &callbacks_factory, &binary_info, &coverage_logger](
-          Environment& my_env, std::atomic<Stats>& stats, bool create_tmpdir) {
-        if (create_tmpdir) CreateLocalDirRemovedAtExit(TemporaryLocalDirPath());
-        // Uses TID, call in this thread.
-        my_env.seed = GetRandomSeed(env.seed);
+  auto fuzzing_worker = [&env, &callbacks_factory, &binary_info,
+                         &coverage_logger, &stop_condition](
+                            Environment& my_env, std::atomic<Stats>& stats,
+                            bool create_tmpdir) {
+    if (create_tmpdir) CreateLocalDirRemovedAtExit(TemporaryLocalDirPath());
+    // Uses TID, call in this thread.
+    my_env.seed = GetRandomSeed(env.seed);
 
-        if (env.dry_run) return;
+    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 @@
 
   if (!env.knobs_file.empty()) PrintRewardValues(stats_vec, std::cerr);
 
-  return ExitCode();
+  return stop_condition.ExitCode();
 }
 
 TestShard SetUpTestSharding() {
@@ -351,7 +342,8 @@
 }
 
 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 @@
   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 @@
   // 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 @@
         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 @@
   }
   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 @@
                      << "\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 @@
         (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 @@
     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 @@
     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 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 @@
       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<std::string, CrashDetails> crashes_by_signature =
@@ -694,9 +692,13 @@
 }  // 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 @@
     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 @@
             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 @@
         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 @@
       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 @@
 
   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 d6c5dfd..818907d 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 @@
 //     InitGoogle(argv[0], &argc, &argv, /*remove_flags=*/true);
 //     fuzztest::internal::Environment env;  // reads FLAGS.
 //     fuzztest::internal::DefaultCallbacksFactory<MyCentipedeCallbacks>
-//     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 8f6a715..e38dcaf 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 <unistd.h>
+
+#include <csignal>
+#include <cstdlib>
+
 #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,6 @@
   fuzztest::internal::DefaultCallbacksFactory<
       fuzztest::internal::CentipedeDefaultCallbacks>
       callbacks;
-  return CentipedeMain(env, callbacks);
+  SetSignalHandlers();
+  return CentipedeMain(env, callbacks, &global_stop_condition);
 }
diff --git a/centipede/centipede_test.cc b/centipede/centipede_test.cc
index 0dd9356..7a7f1d7 100644
--- a/centipede/centipede_test.cc
+++ b/centipede/centipede_test.cc
@@ -74,7 +74,8 @@
 // A mock for CentipedeCallbacks.
 class CentipedeMock : public CentipedeCallbacks {
  public:
-  CentipedeMock(const Environment &env) : CentipedeCallbacks(env) {}
+  CentipedeMock(const Environment& env)
+      : CentipedeCallbacks(env, internal_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 @@
   size_t num_mutations_ = 0;
   size_t max_batch_size_ = 0;
   size_t min_batch_size_ = -1;
+
+ private:
+  StopCondition internal_stop_condition_;
 };
 
 TEST(Centipede, MockTest) {
@@ -352,7 +356,8 @@
 // Callbacks for MutateViaExternalBinary test.
 class MutateCallbacks : public CentipedeCallbacks {
  public:
-  explicit MutateCallbacks(const Environment &env) : CentipedeCallbacks(env) {}
+  explicit MutateCallbacks(const Environment& env)
+      : CentipedeCallbacks(env, internal_stop_condition_) {}
   // Will not be called.
   bool Execute(std::string_view binary, absl::Span<const ByteSpan> inputs,
                BatchResult& batch_result) override {
@@ -368,6 +373,9 @@
 
   // Redeclare a protected member function as public so the tests can call it.
   using CentipedeCallbacks::MutateViaExternalBinary;
+
+ private:
+  StopCondition internal_stop_condition_;
 };
 
 // Maintains `TemporaryLocalDirPath()` during the lifetime.
@@ -389,6 +397,8 @@
   ~CentipedeWithTemporaryLocalDir() override {
     std::filesystem::remove_all(TemporaryLocalDirPath());
   }
+
+  StopCondition stop_condition;
 };
 
 TEST_F(CentipedeWithTemporaryLocalDir, MutateViaExternalBinary) {
@@ -495,7 +505,8 @@
 // A mock for MergeFromOtherCorpus test.
 class MergeMock : public CentipedeCallbacks {
  public:
-  explicit MergeMock(const Environment &env) : CentipedeCallbacks(env) {}
+  explicit MergeMock(const Environment& env)
+      : CentipedeCallbacks(env, internal_stop_condition_) {}
 
   // Doesn't execute anything.
   // All inputs are 1-byte long.
@@ -526,6 +537,7 @@
 
  private:
   size_t number_of_mutations_ = 0;
+  StopCondition internal_stop_condition_;
 };
 
 TEST(Centipede, MergeFromOtherCorpus) {
@@ -578,8 +590,8 @@
 // A mock for FunctionFilter test.
 class FunctionFilterMock : public CentipedeCallbacks {
  public:
-  explicit FunctionFilterMock(const Environment &env)
-      : CentipedeCallbacks(env) {
+  explicit FunctionFilterMock(const Environment& env)
+      : CentipedeCallbacks(env, internal_stop_condition_) {
     std::vector<ByteArray> 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 @@
 
  private:
   size_t number_of_mutations_ = 0;
+  StopCondition internal_stop_condition_;
 };
 
 // Runs a short fuzzing session with the provided `function_filter`.
@@ -695,8 +708,9 @@
 // A mock for ExtraBinaries test.
 class ExtraBinariesMock : public CentipedeCallbacks {
  public:
-  explicit ExtraBinariesMock(const Environment &env, std::vector<Crash> crashes)
-      : CentipedeCallbacks(env), crashes_(std::move(crashes)) {}
+  explicit ExtraBinariesMock(const Environment& env, std::vector<Crash> crashes)
+      : CentipedeCallbacks(env, internal_stop_condition_),
+        crashes_(std::move(crashes)) {}
 
   // Doesn't execute anything.
   // On certain combinations of {binary,input} returns false.
@@ -733,6 +747,7 @@
  private:
   size_t number_of_mutations_ = 0;
   std::vector<Crash> crashes_;
+  StopCondition internal_stop_condition_;
 };
 
 struct FileAndContents {
@@ -811,9 +826,10 @@
 // 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, internal_stop_condition_},
+        crashing_input_idx_{crashing_input_idx} {
     FUZZTEST_CHECK_LE(crashing_input_idx_, std::numeric_limits<uint8_t>::max());
   }
 
@@ -876,6 +892,7 @@
   size_t num_inputs_triaged_ = 0;
   ByteArray crashing_input_ = {};
   bool first_pass_ = true;
+  StopCondition internal_stop_condition_;
 };
 
 // Test for preserving a crashing batch when 1-by-1 exec fails to reproduce.
@@ -943,7 +960,7 @@
   Environment env;
   env.binary =
       GetDataDependencyFilepath("centipede/testing/seeded_fuzz_target");
-  CentipedeDefaultCallbacks callbacks(env);
+  CentipedeDefaultCallbacks callbacks(env, stop_condition);
 
   std::vector<ByteArray> seeds;
   EXPECT_EQ(callbacks.GetSeeds(10, seeds), 10);
@@ -961,7 +978,7 @@
   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 @@
       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 @@
   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<ByteArray> inputs = {{0}};
@@ -1002,9 +1019,10 @@
 
 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, internal_stop_condition_),
+        execute_thread_id_(execute_thread_id) {}
 
   bool Execute(std::string_view binary, absl::Span<const ByteSpan> inputs,
                BatchResult& batch_result) override {
@@ -1024,6 +1042,7 @@
  private:
   std::thread::id execute_thread_id_;
   bool thread_check_passed_ = true;
+  StopCondition internal_stop_condition_;
 };
 
 TEST(Centipede, RunsExecuteCallbackInTheCurrentThreadWhenFuzzingWithOneThread) {
@@ -1046,7 +1065,7 @@
   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<ByteArray> inputs = {ByteArray{'s', 't', 'k'}};
@@ -1059,7 +1078,8 @@
 
 class SetupFailureCallbacks : public CentipedeCallbacks {
  public:
-  using CentipedeCallbacks::CentipedeCallbacks;
+  explicit SetupFailureCallbacks(const Environment& env)
+      : CentipedeCallbacks(env, internal_stop_condition_) {}
 
   bool Execute(std::string_view binary, absl::Span<const ByteSpan> inputs,
                BatchResult& batch_result) override {
@@ -1079,6 +1099,7 @@
 
  private:
   int execute_count_ = 0;
+  StopCondition internal_stop_condition_;
 };
 
 TEST(Centipede, ReturnsFailureOnSetupFailure) {
@@ -1096,7 +1117,8 @@
 
 class SkippedTestCallbacks : public CentipedeCallbacks {
  public:
-  using CentipedeCallbacks::CentipedeCallbacks;
+  explicit SkippedTestCallbacks(const Environment& env)
+      : CentipedeCallbacks(env, internal_stop_condition_) {}
 
   bool Execute(std::string_view binary, absl::Span<const ByteSpan> inputs,
                BatchResult& batch_result) override {
@@ -1117,6 +1139,7 @@
 
  private:
   int execute_count_ = 0;
+  StopCondition internal_stop_condition_;
 };
 
 TEST(Centipede, ReturnsSuccessOnSkippedTest) {
@@ -1134,7 +1157,8 @@
 
 class IgnoredFailureCallbacks : public CentipedeCallbacks {
  public:
-  using CentipedeCallbacks::CentipedeCallbacks;
+  explicit IgnoredFailureCallbacks(const Environment& env)
+      : CentipedeCallbacks(env, internal_stop_condition_) {}
 
   bool Execute(std::string_view binary, absl::Span<const ByteSpan> inputs,
                BatchResult& batch_result) override {
@@ -1155,6 +1179,7 @@
 
  private:
   int execute_count_ = 0;
+  StopCondition internal_stop_condition_;
 };
 
 TEST(Centipede, KeepsRunningAndReturnsSuccessWithIgnoredFailures) {
@@ -1176,7 +1201,7 @@
 class CentipedeMockForInputReduction : public CentipedeCallbacks {
  public:
   CentipedeMockForInputReduction(const Environment& env)
-      : CentipedeCallbacks(env) {}
+      : CentipedeCallbacks(env, internal_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<const ByteSpan> inputs,
@@ -1199,6 +1224,9 @@
     }
     return num_seeds;
   }
+
+ private:
+  StopCondition internal_stop_condition_;
 };
 
 TEST(Centipede, DoesNotReduceInputWhenTheOptionIsUnset) {
@@ -1247,7 +1275,7 @@
   Environment env;
   env.binary = GetDataDependencyFilepath(
       "centipede/testing/fuzz_target_with_custom_mutator");
-  CentipedeDefaultCallbacks callbacks(env);
+  CentipedeDefaultCallbacks callbacks(env, stop_condition);
 
   const std::vector<ByteArray> inputs = {{1}, {2}, {3}, {4}, {5}, {6}};
   const std::vector<Mutant> mutants = callbacks.Mutate(
@@ -1264,23 +1292,22 @@
                        "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<ByteArray> 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<ByteArray> inputs = {{1}, {2}, {3}, {4}, {5}, {6}};
   const std::vector<Mutant> mutants = callbacks.Mutate(
@@ -1295,14 +1322,14 @@
        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<ByteArray> 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 @@
       GetDataDependencyFilepath("centipede/testing/hanging_fuzz_target");
   BatchResult batch_result;
   const std::vector<ByteArray> 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, ExecuteEndsAfterCustomFailure) {
   Environment env;
   env.binary = GetDataDependencyFilepath("centipede/testing/test_fuzz_target");
-  CentipedeDefaultCallbacks callbacks(env);
+  CentipedeDefaultCallbacks callbacks(env, stop_condition);
   BatchResult result;
   std::vector<ByteArray> inputs = {
       {'c', 'u', 's', 't', 'o', 'm'},
@@ -1344,12 +1371,12 @@
   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<ByteArray> 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 4c76598..4afab0a 100644
--- a/centipede/command.cc
+++ b/centipede/command.cc
@@ -434,7 +434,8 @@
   return true;
 }
 
-std::optional<int> Command::Wait(absl::Time deadline) {
+std::optional<int> Command::Wait(absl::Time deadline,
+                                 StopCondition* stop_condition) {
   FUZZTEST_CHECK(is_executing());
   int exit_code = EXIT_SUCCESS;
 
@@ -506,7 +507,9 @@
   } 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 f127248..b574a84 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 @@
 
   // 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<int> Wait(absl::Time deadline);
+  // is executing. If interrupted, may call `stop_condition->RequestEarlyStop()`
+  // (see stop.h).
+  std::optional<int> 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 @@
   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 d7f2f58..4e638b4 100644
--- a/centipede/command_test.cc
+++ b/centipede/command_test.cc
@@ -55,23 +55,25 @@
 }
 
 TEST(CommandTest, Execute) {
+  StopCondition stop_condition;
+
   // Check for default exit code.
   Command echo{"echo"};
-  EXPECT_EQ(echo.Execute(), 0);
-  EXPECT_FALSE(ShouldStop());
+  EXPECT_EQ(echo.Execute(&stop_condition), 0);
+  EXPECT_FALSE(stop_condition.ShouldStop());
 
   // Check for exit code 7.
   Command exit7{"bash -c 'exit 7'"};
-  EXPECT_EQ(exit7.Execute(), 7);
-  EXPECT_FALSE(ShouldStop());
+  EXPECT_EQ(exit7.Execute(&stop_condition), 7);
+  EXPECT_FALSE(stop_condition.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 545eb2d..268c13c 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 @@
     CentipedeCallbacksFactory& callbacks_factory,
     const absl::flat_hash_map<std::string, CrashDetails>&
         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 @@
   std::vector<std::string> 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<std::string, CrashDetails> reproduced_crashes;
diff --git a/centipede/crash_deduplication.h b/centipede/crash_deduplication.h
index f5859eb..936d12a 100644
--- a/centipede/crash_deduplication.h
+++ b/centipede/crash_deduplication.h
@@ -85,7 +85,7 @@
     CentipedeCallbacksFactory& callbacks_factory,
     const absl::flat_hash_map<std::string, CrashDetails>&
         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 f878871..cad5d3f 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 @@
   explicit FakeCentipedeCallbacks(
       const Environment& env,
       absl::flat_hash_map<std::string, Crash> crashing_inputs)
-      : CentipedeCallbacks(env), crashing_inputs_(std::move(crashing_inputs)) {}
+      : CentipedeCallbacks(env, internal_stop_condition_),
+        crashing_inputs_(std::move(crashing_inputs)) {}
 
   bool Execute(std::string_view binary, absl::Span<const ByteSpan> inputs,
                BatchResult& batch_result) override {
@@ -185,6 +187,7 @@
 
  private:
   absl::flat_hash_map<std::string, Crash> crashing_inputs_;
+  StopCondition internal_stop_condition_;
 };
 
 struct FileAndContents {
@@ -228,6 +231,7 @@
   }
   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 @@
   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 @@
   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 @@
   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 @@
   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 @@
   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 @@
   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 @@
   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 @@
   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 @@
   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 @@
   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 @@
   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 @@
   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 @@
   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 @@
   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 @@
   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 @@
   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 @@
   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 @@
   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 245846c..48ca00c 100644
--- a/centipede/minimize_crash.cc
+++ b/centipede/minimize_crash.cc
@@ -85,10 +85,12 @@
 };
 
 // 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 @@
   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 @@
   }
 }
 
-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 @@
   {
     ThreadPool threads{static_cast<int>(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 5677565..2910437 100644
--- a/centipede/minimize_crash.h
+++ b/centipede/minimize_crash.h
@@ -17,6 +17,7 @@
 
 #include "./centipede/centipede_callbacks.h"
 #include "./centipede/environment.h"
+#include "./centipede/stop.h"
 #include "./common/defs.h"
 
 namespace fuzztest::internal {
@@ -28,8 +29,9 @@
 // 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 655c222..9eba004 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 @@
 // A mock for CentipedeCallbacks.
 class MinimizerMock : public CentipedeCallbacks {
  public:
-  MinimizerMock(const Environment &env) : CentipedeCallbacks(env) {}
+  MinimizerMock(const Environment& env)
+      : CentipedeCallbacks(env, internal_stop_condition_) {}
 
   // Runs FuzzMe() on every input, imitates failure if FuzzMe() returns true.
   bool Execute(std::string_view binary, absl::Span<const ByteSpan> inputs,
@@ -65,12 +67,15 @@
     }
     return false;
   }
+
+  StopCondition internal_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 @@
   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<ByteArray> crashers;
   for (auto const &dir_entry : std::filesystem::directory_iterator{
diff --git a/centipede/stop.cc b/centipede/stop.cc
index 6a76c17..b7761a4 100644
--- a/centipede/stop.cc
+++ b/centipede/stop.cc
@@ -21,35 +21,28 @@
 #include "absl/time/time.h"
 
 namespace fuzztest::internal {
-namespace {
 
-struct EarlyStop {
-  int exit_code = EXIT_SUCCESS;
-  bool is_requested = false;
-};
-std::atomic<EarlyStop> early_stop;
-
-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 d3e255d..f000fd5 100644
--- a/centipede/stop.h
+++ b/centipede/stop.h
@@ -15,51 +15,73 @@
 #ifndef THIRD_PARTY_CENTIPEDE_STOP_H_
 #define THIRD_PARTY_CENTIPEDE_STOP_H_
 
+#include <atomic>
+#include <cstdlib>
+
 #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);
+  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 other methods on this object instance. 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<EarlyStop>::is_always_lock_free);
+  std::atomic<EarlyStop> 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 a338524..c9f54f5 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 @@
 // A simple CentipedeCallbacks derivative.
 class TestCallbacks : public CentipedeCallbacks {
  public:
-  explicit TestCallbacks(const Environment &env) : CentipedeCallbacks(env) {}
+  explicit TestCallbacks(const Environment& env)
+      : CentipedeCallbacks(env, internal_stop_condition_) {}
   bool Execute(std::string_view binary, absl::Span<const ByteSpan> inputs,
                BatchResult& batch_result) override {
     int result =
@@ -55,6 +57,9 @@
                              size_t num_mutants) override {
     return {};
   }
+
+ private:
+  StopCondition internal_stop_condition_;
 };
 }  // namespace fuzztest::internal
 
diff --git a/fuzztest/internal/centipede_adaptor.cc b/fuzztest/internal/centipede_adaptor.cc
index aafc9e0..16b46cc 100644
--- a/fuzztest/internal/centipede_adaptor.cc
+++ b/fuzztest/internal/centipede_adaptor.cc
@@ -351,6 +351,8 @@
   return env;
 }
 
+StopCondition global_stop_condition;
+
 void InstallCentipedeTerminationHandler() {
   [[maybe_unused]] static bool install_once = [] {
     for (int signum : {SIGTERM, SIGHUP}) {
@@ -358,7 +360,7 @@
       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 @@
         << "Termination status must be Exited if not Signaled";
     return static_cast<int>(std::get<ExitCodeT>(status.Status()));
   }
+  global_stop_condition.ClearEarlyStopRequestAndSetStopTime(
+      absl::InfiniteFuture());
   static absl::NoDestructor<DefaultCallbacksFactory<CentipedeDefaultCallbacks>>
       factory;
-  return CentipedeMain(env, *factory);
+  return CentipedeMain(env, *factory, &global_stop_condition);
 }
 
 }  // namespace
@@ -1079,7 +1083,8 @@
 
   // 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());