Use the stop time to cap command deadlines when running batches for fuzzing.

Do not report crashes on stop conditions.

Exit early in batch condition when termination is requested.

Prolong the unit testing duration to 10s to reduce flakiness.

This allows the stop time to be enforced more precisely instead of having to wait for the entire batch, without reporting the potential crashes due to interrupting the command.

After this we don't need to set batch timeout to the test time limit - it was a quick workaround for the same requirement.

PiperOrigin-RevId: 791875139
diff --git a/centipede/BUILD b/centipede/BUILD
index 57217a5..16a5573 100644
--- a/centipede/BUILD
+++ b/centipede/BUILD
@@ -665,6 +665,7 @@
         ":runner_request",
         ":runner_result",
         ":shared_memory_blob_sequence",
+        ":stop",
         ":util",
         ":workdir",
         "@abseil-cpp//absl/base:nullability",
diff --git a/centipede/centipede.cc b/centipede/centipede.cc
index 038b4bd..dc41d1f 100644
--- a/centipede/centipede.cc
+++ b/centipede/centipede.cc
@@ -362,9 +362,17 @@
 bool Centipede::ExecuteAndReportCrash(std::string_view binary,
                                       const std::vector<ByteArray> &input_vec,
                                       BatchResult &batch_result) {
-  bool success = user_callbacks_.Execute(binary, input_vec, batch_result);
-  if (!success) ReportCrash(binary, input_vec, batch_result);
-  return success || batch_result.IsIgnoredFailure();
+  bool success =
+      user_callbacks_.Execute(binary, input_vec, batch_result, GetStopTime());
+  if (success) return true;
+  if (ShouldStop()) {
+    FUZZTEST_LOG_FIRST_N(WARNING, 1)
+        << "Stop condition met - not reporting further crashes possibly "
+           "related to the stop condition.";
+    return true;
+  }
+  ReportCrash(binary, input_vec, batch_result);
+  return batch_result.IsIgnoredFailure();
 }
 
 // *** Highly experimental and risky. May not scale well for large targets. ***
@@ -976,7 +984,8 @@
     if (ShouldStop()) return;
     const auto &one_input = input_vec[input_idx];
     BatchResult one_input_batch_result;
-    if (!user_callbacks_.Execute(binary, {one_input}, one_input_batch_result)) {
+    if (!user_callbacks_.Execute(binary, {one_input}, one_input_batch_result,
+                                 absl::InfiniteFuture())) {
       auto hash = Hash(one_input);
       auto crash_dir = wd_.CrashReproducerDirPaths().MyShard();
       FUZZTEST_CHECK_OK(RemoteMkdir(crash_dir));
diff --git a/centipede/centipede_callbacks.cc b/centipede/centipede_callbacks.cc
index 8110dce..07757e5 100644
--- a/centipede/centipede_callbacks.cc
+++ b/centipede/centipede_callbacks.cc
@@ -50,6 +50,7 @@
 #include "./centipede/mutation_input.h"
 #include "./centipede/runner_request.h"
 #include "./centipede/runner_result.h"
+#include "./centipede/stop.h"
 #include "./centipede/util.h"
 #include "./centipede/workdir.h"
 #include "./common/blob_file.h"
@@ -472,14 +473,15 @@
       command_contexts_.end());
 }
 
-int CentipedeCallbacks::RunBatchForBinary(std::string_view binary) {
+int CentipedeCallbacks::RunBatchForBinary(std::string_view binary,
+                                          absl::Time deadline) {
   auto& command_context = GetOrCreateCommandContextForBinary(binary);
   auto& cmd = command_context.cmd;
   const absl::Duration amortized_timeout =
       env_.timeout_per_batch == 0
           ? absl::InfiniteDuration()
           : absl::Seconds(env_.timeout_per_batch) + absl::Seconds(5);
-  const auto deadline = absl::Now() + amortized_timeout;
+  deadline = std::min(absl::Now() + amortized_timeout, deadline);
   int exit_code = EXIT_SUCCESS;
   const bool should_clean_up = [&] {
     if (!cmd.is_executing() && !cmd.ExecuteAsync()) {
@@ -497,11 +499,12 @@
   if (should_clean_up) {
     exit_code = [&] {
       if (!cmd.is_executing()) return EXIT_FAILURE;
-      FUZZTEST_LOG(ERROR) << "Cleaning up the batch execution.";
+      FUZZTEST_LOG(ERROR) << "Cleaning up the batch execution with timeout "
+                          << kCommandCleanupTimeout;
       cmd.RequestStop();
       const auto ret = cmd.Wait(absl::Now() + kCommandCleanupTimeout);
       if (ret.has_value()) return *ret;
-      FUZZTEST_LOG(ERROR) << "Batch execution cleanup failed to end in 60s.";
+      FUZZTEST_LOG(ERROR) << "Failed to wait for the batch execution cleanup.";
       return EXIT_FAILURE;
     }();
     command_contexts_.erase(
@@ -515,7 +518,7 @@
 
 int CentipedeCallbacks::ExecuteCentipedeSancovBinaryWithShmem(
     std::string_view binary, const std::vector<ByteArray>& inputs,
-    BatchResult& batch_result) {
+    BatchResult& batch_result, absl::Time deadline) {
   auto start_time = absl::Now();
   batch_result.ClearAndResize(inputs.size());
 
@@ -541,7 +544,7 @@
   }
 
   // Run.
-  const int exit_code = RunBatchForBinary(binary);
+  const int exit_code = RunBatchForBinary(binary, deadline);
   inputs_blobseq_.ReleaseSharedMemory();  // Inputs are already consumed.
 
   // Get results.
@@ -699,7 +702,8 @@
       << VV(num_inputs_written) << VV(inputs.size());
 
   // Execute.
-  const int exit_code = RunBatchForBinary(binary);
+  const int exit_code =
+      RunBatchForBinary(binary, /*deadline=*/absl::InfiniteFuture());
   inputs_blobseq_.ReleaseSharedMemory();  // Inputs are already consumed.
 
   if (exit_code != EXIT_SUCCESS) {
diff --git a/centipede/centipede_callbacks.h b/centipede/centipede_callbacks.h
index 2976cd1..ea85ea6 100644
--- a/centipede/centipede_callbacks.h
+++ b/centipede/centipede_callbacks.h
@@ -65,8 +65,8 @@
   // Post-condition:
   // `batch_result` has results for every `input`, even on failure.
   virtual bool Execute(std::string_view binary,
-                       const std::vector<ByteArray> &inputs,
-                       BatchResult &batch_result) = 0;
+                       const std::vector<ByteArray>& inputs,
+                       BatchResult& batch_result, absl::Time deadline) = 0;
 
   // Takes non-empty `inputs` and returns at most `num_mutants` mutated inputs.
   virtual std::vector<ByteArray> Mutate(
@@ -103,8 +103,8 @@
   // Same as ExecuteCentipedeSancovBinary, but uses shared memory.
   // Much faster for fast targets since it uses fewer system calls.
   int ExecuteCentipedeSancovBinaryWithShmem(
-      std::string_view binary, const std::vector<ByteArray> &inputs,
-      BatchResult &batch_result);
+      std::string_view binary, const std::vector<ByteArray>& inputs,
+      BatchResult& batch_result, absl::Time deadline);
 
   // Constructs a string CENTIPEDE_RUNNER_FLAGS=":flag1:flag2:...",
   // where the flags are determined by `env` and also include `extra_flags`.
@@ -173,7 +173,7 @@
   // Returns a CommandContext with matching `binary`. Creates one if needed.
   CommandContext& GetOrCreateCommandContextForBinary(std::string_view binary);
   // Runs a batch with the command `binary` and returns the exit code.
-  int RunBatchForBinary(std::string_view binary);
+  int RunBatchForBinary(std::string_view binary, absl::Time deadline);
 
   // Prints the execution log from the last executed binary.
   void PrintExecutionLog() const;
diff --git a/centipede/centipede_default_callbacks.cc b/centipede/centipede_default_callbacks.cc
index ee54c2a..7d128f6 100644
--- a/centipede/centipede_default_callbacks.cc
+++ b/centipede/centipede_default_callbacks.cc
@@ -46,10 +46,11 @@
 }
 
 bool CentipedeDefaultCallbacks::Execute(std::string_view binary,
-                                        const std::vector<ByteArray> &inputs,
-                                        BatchResult &batch_result) {
-  return ExecuteCentipedeSancovBinaryWithShmem(binary, inputs, batch_result) ==
-         0;
+                                        const std::vector<ByteArray>& inputs,
+                                        BatchResult& batch_result,
+                                        absl::Time deadline) {
+  return ExecuteCentipedeSancovBinaryWithShmem(binary, inputs, batch_result,
+                                               deadline) == 0;
 }
 
 size_t CentipedeDefaultCallbacks::GetSeeds(size_t num_seeds,
diff --git a/centipede/centipede_default_callbacks.h b/centipede/centipede_default_callbacks.h
index 0b78562..a3031e0 100644
--- a/centipede/centipede_default_callbacks.h
+++ b/centipede/centipede_default_callbacks.h
@@ -40,8 +40,8 @@
   explicit CentipedeDefaultCallbacks(const Environment &env);
   size_t GetSeeds(size_t num_seeds, std::vector<ByteArray> &seeds) override;
   absl::StatusOr<std::string> GetSerializedTargetConfig() override;
-  bool Execute(std::string_view binary, const std::vector<ByteArray> &inputs,
-               BatchResult &batch_result) override;
+  bool Execute(std::string_view binary, const std::vector<ByteArray>& inputs,
+               BatchResult& batch_result, absl::Time deadline) override;
   std::vector<ByteArray> Mutate(const std::vector<MutationInputRef> &inputs,
                                 size_t num_mutants) override;
 
diff --git a/centipede/centipede_interface.cc b/centipede/centipede_interface.cc
index 0c6948f..26b70ec 100644
--- a/centipede/centipede_interface.cc
+++ b/centipede/centipede_interface.cc
@@ -305,7 +305,7 @@
     FUZZTEST_CHECK_OK(
         RemoteFileGetContents(crashing_input_file, crashing_input));
     const bool is_reproducible = !scoped_callbacks.callbacks()->Execute(
-        env.binary, {crashing_input}, batch_result);
+        env.binary, {crashing_input}, batch_result, absl::InfiniteFuture());
     const bool is_duplicate =
         is_reproducible && !batch_result.IsSetupFailure() &&
         !remaining_crash_signatures.insert(batch_result.failure_signature())
@@ -686,6 +686,8 @@
           << "Skip updating corpus database due to early stop requested.";
       continue;
     }
+    // The test time limit does not apply for the rest of the steps.
+    ClearEarlyStopRequestAndSetStopTime(/*stop_time=*/absl::InfiniteFuture());
 
     // TODO(xinhaoyuan): Have a separate flag to skip corpus updating instead
     // of checking whether workdir is specified or not.
diff --git a/centipede/centipede_test.cc b/centipede/centipede_test.cc
index 39ce844..e44c8a0 100644
--- a/centipede/centipede_test.cc
+++ b/centipede/centipede_test.cc
@@ -68,8 +68,8 @@
   // 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.
-  bool Execute(std::string_view binary, const std::vector<ByteArray> &inputs,
-               BatchResult &batch_result) override {
+  bool Execute(std::string_view binary, const std::vector<ByteArray>& inputs,
+               BatchResult& batch_result, absl::Time deadline) override {
     batch_result.results().clear();
     // For every input, we create a 256-element array `counters`, where
     // i-th element is the number of bytes with the value 'i' in the input.
@@ -356,8 +356,8 @@
  public:
   explicit MutateCallbacks(const Environment &env) : CentipedeCallbacks(env) {}
   // Will not be called.
-  bool Execute(std::string_view binary, const std::vector<ByteArray> &inputs,
-               BatchResult &batch_result) override {
+  bool Execute(std::string_view binary, const std::vector<ByteArray>& inputs,
+               BatchResult& batch_result, absl::Time deadline) override {
     FUZZTEST_LOG(FATAL);
     return false;
   }
@@ -499,8 +499,8 @@
   // Doesn't execute anything.
   // All inputs are 1-byte long.
   // For an input {X}, the feature output is {X}.
-  bool Execute(std::string_view binary, const std::vector<ByteArray> &inputs,
-               BatchResult &batch_result) override {
+  bool Execute(std::string_view binary, const std::vector<ByteArray>& inputs,
+               BatchResult& batch_result, absl::Time deadline) override {
     batch_result.results().resize(inputs.size());
     for (size_t i = 0, n = inputs.size(); i < n; ++i) {
       FUZZTEST_CHECK_EQ(inputs[i].size(), 1);
@@ -588,10 +588,10 @@
   }
 
   // Executes the target in the normal way.
-  bool Execute(std::string_view binary, const std::vector<ByteArray> &inputs,
-               BatchResult &batch_result) override {
-    return ExecuteCentipedeSancovBinaryWithShmem(env_.binary, inputs,
-                                                 batch_result) == EXIT_SUCCESS;
+  bool Execute(std::string_view binary, const std::vector<ByteArray>& inputs,
+               BatchResult& batch_result, absl::Time deadline) override {
+    return ExecuteCentipedeSancovBinaryWithShmem(
+               env_.binary, inputs, batch_result, deadline) == EXIT_SUCCESS;
   }
 
   // Sets the inputs to one of 3 pre-defined values.
@@ -696,8 +696,8 @@
 
   // Doesn't execute anything.
   // On certain combinations of {binary,input} returns false.
-  bool Execute(std::string_view binary, const std::vector<ByteArray> &inputs,
-               BatchResult &batch_result) override {
+  bool Execute(std::string_view binary, const std::vector<ByteArray>& inputs,
+               BatchResult& batch_result, absl::Time deadline) override {
     bool res = true;
     for (const auto &input : inputs) {
       if (input.size() != 1) continue;
@@ -814,8 +814,8 @@
   // Doesn't execute anything.
   // Crash when 0th char of input to binary b1 equals `crashing_input_idx_`, but
   // only on 1st exec.
-  bool Execute(std::string_view binary, const std::vector<ByteArray> &inputs,
-               BatchResult &batch_result) override {
+  bool Execute(std::string_view binary, const std::vector<ByteArray>& inputs,
+               BatchResult& batch_result, absl::Time deadline) override {
     batch_result.ClearAndResize(inputs.size());
     bool res = true;
     if (!first_pass_) {
@@ -982,7 +982,8 @@
 
   BatchResult batch_result;
   const std::vector<ByteArray> inputs = {{0}};
-  ASSERT_TRUE(callbacks.Execute(env.binary, inputs, batch_result));
+  ASSERT_TRUE(callbacks.Execute(env.binary, inputs, batch_result,
+                                /*deadline=*/absl::InfiniteFuture()));
   ASSERT_EQ(batch_result.results().size(), 1);
   bool found_startup_cmp_entry = false;
   batch_result.results()[0].metadata().ForEachCmpEntry(
@@ -999,8 +1000,8 @@
                                           std::thread::id execute_thread_id)
       : CentipedeCallbacks(env), execute_thread_id_(execute_thread_id) {}
 
-  bool Execute(std::string_view binary, const std::vector<ByteArray> &inputs,
-               BatchResult &batch_result) override {
+  bool Execute(std::string_view binary, const std::vector<ByteArray>& inputs,
+               BatchResult& batch_result, absl::Time deadline) override {
     batch_result.ClearAndResize(inputs.size());
     thread_check_passed_ = thread_check_passed_ &&
                            std::this_thread::get_id() == execute_thread_id_;
@@ -1044,7 +1045,8 @@
   BatchResult batch_result;
   const std::vector<ByteArray> inputs = {ByteArray{'s', 't', 'k'}};
 
-  ASSERT_FALSE(callbacks.Execute(env.binary, inputs, batch_result));
+  ASSERT_FALSE(callbacks.Execute(env.binary, inputs, batch_result,
+                                 /*deadline=*/absl::InfiniteFuture()));
   EXPECT_THAT(batch_result.log(), HasSubstr("Stack limit exceeded"));
   EXPECT_EQ(batch_result.failure_description(), "stack-limit-exceeded");
 }
@@ -1053,8 +1055,8 @@
  public:
   using CentipedeCallbacks::CentipedeCallbacks;
 
-  bool Execute(std::string_view binary, const std::vector<ByteArray> &inputs,
-               BatchResult &batch_result) override {
+  bool Execute(std::string_view binary, const std::vector<ByteArray>& inputs,
+               BatchResult& batch_result, absl::Time deadline) override {
     ++execute_count_;
     batch_result.ClearAndResize(inputs.size());
     batch_result.exit_code() = EXIT_FAILURE;
@@ -1090,8 +1092,8 @@
  public:
   using CentipedeCallbacks::CentipedeCallbacks;
 
-  bool Execute(std::string_view binary, const std::vector<ByteArray> &inputs,
-               BatchResult &batch_result) override {
+  bool Execute(std::string_view binary, const std::vector<ByteArray>& inputs,
+               BatchResult& batch_result, absl::Time deadline) override {
     ++execute_count_;
     batch_result.ClearAndResize(inputs.size());
     batch_result.exit_code() = EXIT_FAILURE;
@@ -1128,8 +1130,8 @@
  public:
   using CentipedeCallbacks::CentipedeCallbacks;
 
-  bool Execute(std::string_view binary, const std::vector<ByteArray> &inputs,
-               BatchResult &batch_result) override {
+  bool Execute(std::string_view binary, const std::vector<ByteArray>& inputs,
+               BatchResult& batch_result, absl::Time daedline) override {
     ++execute_count_;
     batch_result.ClearAndResize(inputs.size());
     batch_result.exit_code() = EXIT_FAILURE;
@@ -1224,7 +1226,8 @@
   env.fork_server = false;
 
   // Test that the process does not get stuck and exits promptly.
-  EXPECT_FALSE(callbacks.Execute(env.binary, {{0}}, batch_result));
+  EXPECT_FALSE(callbacks.Execute(env.binary, {{0}}, batch_result,
+                                 /*deadline=*/absl::InfiniteFuture()));
 }
 
 }  // namespace
diff --git a/centipede/environment.cc b/centipede/environment.cc
index 4cd2be6..bed7922 100644
--- a/centipede/environment.cc
+++ b/centipede/environment.cc
@@ -285,17 +285,6 @@
   timeout_per_input = time_limit_per_input_sec;
   UpdateTimeoutPerBatchIfEqualTo(autocomputed_timeout_per_batch);
 
-  // Adjust `timeout_per_batch` to never exceed the test time limit.
-  if (const auto test_time_limit = config.GetTimeLimitPerTest();
-      test_time_limit < absl::InfiniteDuration()) {
-    const size_t test_time_limit_seconds =
-        convert_to_seconds(test_time_limit, "Test time limit");
-    timeout_per_batch =
-        timeout_per_batch == 0
-            ? test_time_limit_seconds
-            : std::min(timeout_per_batch, test_time_limit_seconds);
-  }
-
   // Convert bytes to MB by rounding up.
   constexpr auto bytes_to_mb = [](size_t bytes) {
     return bytes == 0 ? 0 : (bytes - 1) / 1024 / 1024 + 1;
diff --git a/centipede/environment_test.cc b/centipede/environment_test.cc
index a906b99..9776192 100644
--- a/centipede/environment_test.cc
+++ b/centipede/environment_test.cc
@@ -147,30 +147,6 @@
   EXPECT_EQ(env.timeout_per_batch, 0);
 }
 
-TEST(Environment, UpdatesTimeoutPerBatchFromTargetConfigTimeLimit) {
-  Environment env;
-  fuzztest::internal::Configuration config;
-  config.time_limit = absl::Seconds(123);
-  config.time_budget_type = fuzztest::internal::TimeBudgetType::kPerTest;
-  FUZZTEST_CHECK(config.GetTimeLimitPerTest() == absl::Seconds(123));
-  env.UpdateWithTargetConfig(config);
-  EXPECT_EQ(env.timeout_per_batch, 123)
-      << "`timeout_per_batch` should be set to the test time limit when it was "
-         "previously unset";
-
-  env.timeout_per_batch = 456;
-  env.UpdateWithTargetConfig(config);
-  EXPECT_EQ(env.timeout_per_batch, 123)
-      << "`timeout_per_batch` should be set to test time limit when it is "
-         "shorter than the previous value";
-
-  env.timeout_per_batch = 56;
-  env.UpdateWithTargetConfig(config);
-  EXPECT_EQ(env.timeout_per_batch, 56)
-      << "`timeout_per_batch` should not be updated with the test time limit "
-         "when it is longer than the previous value";
-}
-
 TEST(Environment, UpdatesRssLimitMbFromTargetConfigRssLimit) {
   Environment env;
   env.rss_limit_mb = Environment::Default().rss_limit_mb;
diff --git a/centipede/minimize_crash.cc b/centipede/minimize_crash.cc
index 66d417a..dda7d21 100644
--- a/centipede/minimize_crash.cc
+++ b/centipede/minimize_crash.cc
@@ -122,7 +122,8 @@
     }
 
     // Execute all mutants. If a new crasher is found, add it to `queue`.
-    if (!callbacks->Execute(env.binary, smaller_mutants, batch_result)) {
+    if (!callbacks->Execute(env.binary, smaller_mutants, batch_result,
+                            absl::InfiniteFuture())) {
       size_t crash_inputs_idx = batch_result.num_outputs_read();
       FUZZTEST_CHECK_LT(crash_inputs_idx, smaller_mutants.size());
       const auto &new_crasher = smaller_mutants[crash_inputs_idx];
@@ -142,7 +143,8 @@
 
   BatchResult batch_result;
   ByteArray original_crashy_input(crashy_input.begin(), crashy_input.end());
-  if (callbacks->Execute(env.binary, {original_crashy_input}, batch_result)) {
+  if (callbacks->Execute(env.binary, {original_crashy_input}, batch_result,
+                         absl::InfiniteFuture())) {
     FUZZTEST_LOG(INFO) << "The original crashy input did not crash; exiting";
     return EXIT_FAILURE;
   }
diff --git a/centipede/minimize_crash_test.cc b/centipede/minimize_crash_test.cc
index 30a145a..5011e86 100644
--- a/centipede/minimize_crash_test.cc
+++ b/centipede/minimize_crash_test.cc
@@ -40,8 +40,8 @@
   MinimizerMock(const Environment &env) : CentipedeCallbacks(env) {}
 
   // Runs FuzzMe() on every input, imitates failure if FuzzMe() returns true.
-  bool Execute(std::string_view binary, const std::vector<ByteArray> &inputs,
-               BatchResult &batch_result) override {
+  bool Execute(std::string_view binary, const std::vector<ByteArray>& inputs,
+               BatchResult& batch_result, absl::Time deadline) override {
     batch_result.ClearAndResize(inputs.size());
     for (auto &input : inputs) {
       if (FuzzMe(input)) {
diff --git a/centipede/runner.cc b/centipede/runner.cc
index bd6e44c..519e615 100644
--- a/centipede/runner.cc
+++ b/centipede/runner.cc
@@ -173,6 +173,8 @@
             resource.what, resource.limit, resource.units, resource.value);
         CentipedeSetFailureDescription(resource.failure);
         std::abort();
+      } else {
+        while (true) sleep(1);  // NOLINT
       }
     }
   }
@@ -493,12 +495,14 @@
 
     RunOneInput(data.data(), data.size(), callbacks);
 
+    if (state->has_failure_description.load()) break;
+
     if (!FinishSendingOutputsToEngine(outputs_blobseq)) break;
   }
 
   CentipedeEndExecutionBatch();
 
-  return EXIT_SUCCESS;
+  return state->has_failure_description.load() ? EXIT_FAILURE : EXIT_SUCCESS;
 }
 
 // Dumps seed inputs to `output_dir`. Also see `GetSeedsViaExternalBinary()`.
@@ -1011,23 +1015,21 @@
 extern "C" void CentipedeSetFailureDescription(const char *description) {
   using fuzztest::internal::state;
   if (state->failure_description_path == nullptr) return;
-  // Make sure that the write is atomic and only happens once.
-  [[maybe_unused]] static int write_once = [=] {
-    FILE* f = fopen(state->failure_description_path, "w");
-    if (f == nullptr) {
-      perror("FAILURE: fopen()");
-      return 0;
-    }
-    const auto len = strlen(description);
-    if (fwrite(description, 1, len, f) != len) {
-      perror("FAILURE: fwrite()");
-    }
-    if (fflush(f) != 0) {
-      perror("FAILURE: fflush()");
-    }
-    if (fclose(f) != 0) {
-      perror("FAILURE: fclose()");
-    }
-    return 0;
-  }();
+  if (state->has_failure_description.exchange(true)) return;
+  FILE* f = fopen(state->failure_description_path, "w");
+  if (f == nullptr) {
+    perror("FAILURE: fopen()");
+    return;
+  }
+  const auto len = strlen(description);
+  if (fwrite(description, 1, len, f) != len) {
+    perror("FAILURE: fwrite()");
+  }
+  if (fflush(f) != 0) {
+    perror("FAILURE: fflush()");
+  }
+  if (fclose(f) != 0) {
+    perror("FAILURE: fclose()");
+  }
+  return;
 }
diff --git a/centipede/runner.h b/centipede/runner.h
index 7eac416..01e0c0c 100644
--- a/centipede/runner.h
+++ b/centipede/runner.h
@@ -80,6 +80,8 @@
   const char *failure_description_path =
       flag_helper.GetStringFlag(":failure_description_path=");
 
+  std::atomic<bool> has_failure_description = false;
+
   const char* persistent_mode_socket_path =
       flag_helper.GetStringFlag(":persistent_mode_socket=");
   int persistent_mode_socket = 0;
diff --git a/centipede/runner_interface.h b/centipede/runner_interface.h
index 96f1a8d..eb004d5 100644
--- a/centipede/runner_interface.h
+++ b/centipede/runner_interface.h
@@ -132,6 +132,9 @@
 
 // Set the failure description for the runner to propagate further. Only the
 // description from the first call will be used.
+//
+// If used during executing batch inputs, the rest of the inputs would be
+// skipped and the batch would be considered as failed.
 extern "C" void CentipedeSetFailureDescription(const char *description);
 
 namespace fuzztest::internal {
diff --git a/centipede/stop.cc b/centipede/stop.cc
index 20f85bd..6a76c17 100644
--- a/centipede/stop.cc
+++ b/centipede/stop.cc
@@ -46,6 +46,8 @@
   early_stop.store({exit_code, true}, std::memory_order_release);
 }
 
+absl::Time GetStopTime() { return stop_time; }
+
 bool ShouldStop() { return EarlyStopRequested() || stop_time < absl::Now(); }
 
 int ExitCode() { return early_stop.load(std::memory_order_acquire).exit_code; }
diff --git a/centipede/stop.h b/centipede/stop.h
index f4244f8..d3e255d 100644
--- a/centipede/stop.h
+++ b/centipede/stop.h
@@ -46,6 +46,13 @@
 // ENSURES: Thread-safe.
 bool ShouldStop();
 
+// Returns the stop time set from the recent
+// `ClearEarlyStopRequestAndSetStopTime()`, or `absl::InfiniteFuture()` it was
+// not set.
+//
+// ENSURES: Thread-safe.
+absl::Time GetStopTime();
+
 // Returns the value most recently passed to `RequestEarlyStop()` or 0 if
 // `RequestEarlyStop()` was not called since the most recent call to
 // `ClearEarlyStopRequestAndSetStopTime()` (if any).
diff --git a/centipede/test_coverage_util.cc b/centipede/test_coverage_util.cc
index 2602a58..ec34885 100644
--- a/centipede/test_coverage_util.cc
+++ b/centipede/test_coverage_util.cc
@@ -41,7 +41,8 @@
   }
   BatchResult batch_result;
   // Run.
-  CBs.Execute(env.binary, byte_array_inputs, batch_result);
+  CBs.Execute(env.binary, byte_array_inputs, batch_result,
+              /*deadline=*/absl::InfiniteFuture());
 
   // Repackage execution results into a vector of CorpusRecords.
   std::vector<CorpusRecord> corpus_records;
diff --git a/centipede/test_coverage_util.h b/centipede/test_coverage_util.h
index c06a9bf..ad8800b 100644
--- a/centipede/test_coverage_util.h
+++ b/centipede/test_coverage_util.h
@@ -43,10 +43,10 @@
 class TestCallbacks : public CentipedeCallbacks {
  public:
   explicit TestCallbacks(const Environment &env) : CentipedeCallbacks(env) {}
-  bool Execute(std::string_view binary, const std::vector<ByteArray> &inputs,
-               BatchResult &batch_result) override {
-    int result =
-        ExecuteCentipedeSancovBinaryWithShmem(binary, inputs, batch_result);
+  bool Execute(std::string_view binary, const std::vector<ByteArray>& inputs,
+               BatchResult& batch_result, absl::Time deadline) override {
+    int result = ExecuteCentipedeSancovBinaryWithShmem(binary, inputs,
+                                                       batch_result, deadline);
     FUZZTEST_CHECK_EQ(EXIT_SUCCESS, result);
     return true;
   }
diff --git a/e2e_tests/corpus_database_test.cc b/e2e_tests/corpus_database_test.cc
index caa1b8c..2c631a7 100644
--- a/e2e_tests/corpus_database_test.cc
+++ b/e2e_tests/corpus_database_test.cc
@@ -38,7 +38,7 @@
 #define EXPECT_THAT_LOG(log, matcher)                                  \
   EXPECT_TRUE(testing::Value(log, matcher))                            \
       << "Matcher: " << testing::DescribeMatcher<std::string>(matcher) \
-      << "Contents of " #log ":\n"                                     \
+      << "\nContents of " #log ":\n"                                   \
       << log
 
 namespace fuzztest::internal {
@@ -123,6 +123,7 @@
     // Dumping stack trace in gtest would slow down the execution, causing
     // test flakiness.
     options.flags[GTEST_FLAG_PREFIX_ "stack_trace_depth"] = "0";
+    options.flags["symbolize_stacktrace"] = "0";
     switch (GetParam()) {
       case ExecutionModelParam::kTestBinary:
         return RunBinary(binary_path, options);
diff --git a/e2e_tests/functional_test.cc b/e2e_tests/functional_test.cc
index b651847..5fb1251 100644
--- a/e2e_tests/functional_test.cc
+++ b/e2e_tests/functional_test.cc
@@ -142,6 +142,7 @@
   auto [status, std_out, std_err] =
       Run(/*test_filter=*/"*",
           /*target_binary=*/"testdata/fuzz_tests_with_invalid_seeds");
+  SCOPED_TRACE(std_err);
   EXPECT_THAT_LOG(std_err, HasSubstr("[!] Skipping WithSeeds() value in"));
   EXPECT_THAT_LOG(std_err,
                   HasSubstr("Could not turn value into corpus type:\n{17}"));
@@ -253,6 +254,7 @@
 
 TEST_F(UnitTestModeTest, FixtureGoesThroughCompleteLifecycle) {
   auto [status, std_out, std_err] = Run("FixtureTest.NeverFails");
+  SCOPED_TRACE(std_err);
   EXPECT_GT(CountSubstrs(std_err, "<<FixtureTest::FixtureTest()>>"), 0);
   EXPECT_EQ(CountSubstrs(std_err, "<<FixtureTest::FixtureTest()>>"),
             CountSubstrs(std_err, "<<FixtureTest::~FixtureTest()>>"));
@@ -276,6 +278,7 @@
   auto [status, std_out, std_err] =
       Run("CallCountPerFuzzTest.CallCountPerFuzzTestEqualsToGlobalCount:"
           "CallCountPerFuzzTest.NeverFails");
+  SCOPED_TRACE(std_err);
   EXPECT_GT(CountSubstrs(std_err, "<<CallCountGoogleTest::SetUpTestSuite()>>"),
             0);
   EXPECT_EQ(
@@ -600,6 +603,7 @@
   auto [status, std_out, std_err] =
       Run("MySuite.DataDependentStackOverflow", kDefaultTargetBinary,
           /*env=*/{}, /*fuzzer_flags=*/{{"stack_limit_kb", "1000"}});
+  SCOPED_TRACE(std_err);
   EXPECT_THAT_LOG(std_err, HasSubstr("argument 0: "));
   ExpectStackLimitExceededMessage(std_err, 1024000);
   ExpectTargetAbort(status, std_err);
@@ -608,7 +612,7 @@
 TEST_F(UnitTestModeTest, RssLimitFlagWorks) {
   auto [status, std_out, std_err] =
       Run("MySuite.LargeHeapAllocation", kDefaultTargetBinary,
-          /*env=*/{}, /*fuzzer_flags=*/{{"rss_limit_mb", "1024"}});
+          /*env=*/{}, /*fuzzer_flags=*/{{"rss_limit_mb", "2048"}});
   EXPECT_THAT_LOG(std_err, HasSubstr("argument 0: "));
   EXPECT_THAT_LOG(std_err, ContainsRegex(absl::StrCat("RSS limit exceeded")));
   ExpectTargetAbort(status, std_err);
@@ -619,6 +623,7 @@
       Run("MySuite.Sleep", kDefaultTargetBinary,
           /*env=*/{},
           /*fuzzer_flags=*/{{"time_limit_per_input", "1s"}});
+  SCOPED_TRACE(std_err);
   EXPECT_THAT_LOG(std_err, HasSubstr("argument 0: "));
   EXPECT_THAT_LOG(std_err, ContainsRegex("Per-input timeout exceeded"));
   ExpectTargetAbort(status, std_err);
@@ -1164,9 +1169,10 @@
 // to restrict the filter to only fuzz tests.
 TEST_F(FuzzingModeCommandLineInterfaceTest, RunsOnlyFuzzTests) {
   auto [status, std_out, std_err] =
-      RunWith({{"fuzz_for", "1ns"}}, /*env=*/{}, /*timeout=*/absl::Seconds(10),
+      RunWith({{"fuzz_for", "1s"}}, /*env=*/{}, /*timeout=*/absl::Seconds(10),
               "testdata/unit_test_and_fuzz_tests");
 
+  SCOPED_TRACE(std_err);
   EXPECT_THAT_LOG(std_out,
                   Not(HasSubstr("[ RUN      ] UnitTest.AlwaysPasses")));
   EXPECT_THAT_LOG(std_out, HasSubstr("[ RUN      ] FuzzTest.AlwaysPasses"));
@@ -1178,7 +1184,7 @@
 TEST_F(FuzzingModeCommandLineInterfaceTest,
        AllowsSpecifyingFilterWithFuzzForDuration) {
   auto [status, std_out, std_err] =
-      RunWith({{"fuzz_for", "1ns"}}, /*env=*/{}, /*timeout=*/absl::Seconds(10),
+      RunWith({{"fuzz_for", "1s"}}, /*env=*/{}, /*timeout=*/absl::Seconds(10),
               "testdata/unit_test_and_fuzz_tests",
               {{GTEST_FLAG_PREFIX_ "filter",
                 "UnitTest.AlwaysPasses:FuzzTest.AlwaysPasses"}});
@@ -1227,6 +1233,7 @@
       std_err,
       HasSubstr("Starting the update of the corpus database for fuzz tests"));
   EXPECT_THAT_LOG(std_err, HasSubstr("FuzzTest.AlwaysPasses"));
+  SCOPED_TRACE(std_err);
   EXPECT_THAT(status, Eq(ExitCode(0)));
 }
 
diff --git a/e2e_tests/testdata/fuzz_tests_for_functional_testing.cc b/e2e_tests/testdata/fuzz_tests_for_functional_testing.cc
index 26368c8..8403f8b 100644
--- a/e2e_tests/testdata/fuzz_tests_for_functional_testing.cc
+++ b/e2e_tests/testdata/fuzz_tests_for_functional_testing.cc
@@ -865,8 +865,8 @@
 }
 FUZZ_TEST(MySuite, LargeHeapAllocation)
     .WithDomains(Just(
-        // 1 GiB
-        1ULL << 30));
+        // 2 GiB
+        1ULL << 31));
 
 // A fuzz test that is expected to accept and skip some inputs before hitting
 // the crash.
diff --git a/fuzztest/internal/centipede_adaptor.cc b/fuzztest/internal/centipede_adaptor.cc
index 54479c4..cfdfc59 100644
--- a/fuzztest/internal/centipede_adaptor.cc
+++ b/fuzztest/internal/centipede_adaptor.cc
@@ -194,7 +194,7 @@
     const Configuration& configuration, absl::string_view workdir,
     absl::string_view test_name, RunMode run_mode) {
   fuzztest::internal::Environment env = CreateDefaultCentipedeEnvironment();
-  constexpr absl::Duration kUnitTestDefaultDuration = absl::Seconds(3);
+  constexpr absl::Duration kUnitTestDefaultDuration = absl::Seconds(10);
   env.fuzztest_multi_test_mode_soon_to_be_removed = false;
   if (configuration.time_limit_per_input < absl::InfiniteDuration()) {
     const int64_t time_limit_seconds =
@@ -462,25 +462,27 @@
         prng_(GetRandomSeed()) {}
 
   bool Execute(fuzztest::internal::ByteSpan input) override {
-    [[maybe_unused]] static bool check_if_not_skipped_on_setup = [&] {
-      if (runtime_.skipping_requested()) {
-        absl::FPrintF(GetStderr(),
-                      "[.] Skipping %s per request from the test setup.\n",
-                      fuzzer_impl_.test_.full_name());
-        CentipedeSetFailureDescription("SKIPPED TEST: Requested from setup");
-        // It has to use _Exit(1) to avoid trigger the reporting of regular
-        // setup failure while let Centipede be aware of this. Note that this
-        // skips the fixture teardown.
-        std::_Exit(1);
-      }
-      return true;
-    }();
     // Disable tracing until running the property function in
     // `CentipedeFxitureDriver::RunFuzzTestIteration()`
     const int old_traced = CentipedeSetCurrentThreadTraced(/*traced=*/0);
     absl::Cleanup tracing_restorer = [old_traced] {
       CentipedeSetCurrentThreadTraced(old_traced);
     };
+    static const bool skipped_on_setup = runtime_.skipping_requested();
+    if (skipped_on_setup) {
+      absl::FPrintF(GetStderr(),
+                    "[.] Skipping %s per request from the test setup.\n",
+                    fuzzer_impl_.test_.full_name());
+      CentipedeSetFailureDescription("SKIPPED TEST: Requested from setup");
+      return true;
+    }
+    if (runtime_.termination_requested()) {
+      absl::FPrintF(GetStderr(),
+                    "[.] Termination requested - exiting without executing "
+                    "further inputs.\n");
+      CentipedeSetFailureDescription("IGNORED FAILURE: Termination requested");
+      return false;
+    }
     // We should avoid doing anything other than executing the input here so
     // that we don't affect the execution time.
     auto parsed_input =
@@ -566,6 +568,13 @@
   ~CentipedeAdaptorRunnerCallbacks() override { runtime_.UnsetCurrentArgs(); }
 
  private:
+  void ExitWithFailureDescription(const char* description) {
+    // It has to use _Exit(1) to avoid trigger the reporting of regular
+    // setup failure while let Centipede be aware of this. Note that this
+    // skips the fixture teardown.
+    std::_Exit(1);
+  }
+
   template <typename T>
   static void InsertCmpEntryIntoIntegerDictionary(
       const uint8_t* a, const uint8_t* b, TablesOfRecentCompares& cmp_tables) {
@@ -1052,7 +1061,8 @@
 
   bool Execute(std::string_view binary,
                const std::vector<fuzztest::internal::ByteArray>& inputs,
-               fuzztest::internal::BatchResult& batch_result) override {
+               fuzztest::internal::BatchResult& batch_result,
+               absl::Time deadline) override {
     return false;
   }