No public description

PiperOrigin-RevId: 989659763
diff --git a/centipede/centipede_interface.cc b/centipede/centipede_interface.cc
index e6ba1eb..1e26b95 100644
--- a/centipede/centipede_interface.cc
+++ b/centipede/centipede_interface.cc
@@ -75,6 +75,7 @@
 namespace {
 
 constexpr absl::Duration kDefaultRegressionTtl = absl::Hours(24 * 7);
+constexpr double kCrashDeduplicationTimeFraction = 0.25;
 
 // Runs env.for_each_blob on every blob extracted from env.args.
 // Returns EXIT_SUCCESS on success, EXIT_FAILURE otherwise.
@@ -419,6 +420,14 @@
   }
 }
 
+absl::Duration GetCrashDeduplicationTimeLimit(
+    absl::Duration fuzzing_time_limit) {
+  if (fuzzing_time_limit == absl::InfiniteDuration()) {
+    return absl::InfiniteDuration();
+  }
+  return kCrashDeduplicationTimeFraction * fuzzing_time_limit;
+}
+
 void UpdateCorpusDatabase(Environment env,
                           CentipedeCallbacksFactory& callbacks_factory,
                           StopCondition& stop_condition) {
@@ -610,8 +619,11 @@
     return;
   }
 
-  // The test time limit does not apply for updating the corpus database.
-  stop_condition.SetStopTime(absl::InfiniteFuture());
+  const absl::Duration dedup_time_limit =
+      GetCrashDeduplicationTimeLimit(time_limit);
+  FUZZTEST_LOG(INFO) << "Dedicating " << dedup_time_limit
+                     << " to updating corpus database for " << env.test_name;
+  stop_condition.SetStopTime(absl::Now() + dedup_time_limit);
   RecordFuzzingResults(env, db_paths, callbacks_factory, stop_condition);
 }
 
diff --git a/centipede/crash_deduplication.cc b/centipede/crash_deduplication.cc
index 3971105..0f5f4a7 100644
--- a/centipede/crash_deduplication.cc
+++ b/centipede/crash_deduplication.cc
@@ -152,58 +152,72 @@
   return incubating_crashes;
 }
 
-absl::Status ReplayCrash(CentipedeCallbacks& callbacks, const Environment& env,
-                         absl::string_view input_path,
-                         std::string& out_signature,
-                         std::string& out_description) {
-  ByteArray input_bytes;
-  RETURN_IF_NOT_OK(RemoteFileGetContents(input_path, input_bytes));
+struct CrashToReplay {
+  CrashDetails& details;
+  std::string& new_signature;
+  ByteArray input_bytes = {};
+};
+
+std::optional<CrashToReplay> ToCrashToReplay(ExistingCrash& existing) {
+  if (existing.crash_report.signature.empty()) return std::nullopt;
+  return CrashToReplay{existing.crash_report.details, existing.new_signature};
+}
+
+std::optional<CrashToReplay> ToCrashToReplay(IncubatingCrash& incubating) {
+  return CrashToReplay{incubating.details, incubating.new_signature};
+}
+
+template <typename CrashT>
+absl::StatusOr<std::vector<CrashToReplay>> ToCrashesToReplay(
+    absl::Span<CrashT> raw_crashes) {
+  std::vector<CrashToReplay> crashes;
+  crashes.reserve(raw_crashes.size());
+  for (CrashT& raw_crash : raw_crashes) {
+    std::optional<CrashToReplay> crash = ToCrashToReplay(raw_crash);
+    if (!crash.has_value()) continue;
+    RETURN_IF_NOT_OK(
+        RemoteFileGetContents(crash->details.input_path, crash->input_bytes));
+    crashes.push_back(*std::move(crash));
+  }
+  return crashes;
+}
+
+absl::Status ReplayCrashes(CentipedeCallbacks& callbacks,
+                           const Environment& env,
+                           const StopCondition& stop_condition,
+                           absl::Span<CrashToReplay> crashes) {
+  for (CrashToReplay& crash : crashes) {
+    crash.new_signature.clear();
+    crash.details.description.clear();
+  }
 
   const size_t max_attempts = std::max<size_t>(1, env.replay_crash_attempts);
   for (size_t attempt = 0; attempt < max_attempts; ++attempt) {
-    BatchResult batch_result;
-    if (!callbacks.Execute(env.binary, {input_bytes}, batch_result) &&
-        batch_result.IsInputFailure()) {
-      out_signature = batch_result.failure_signature();
-      out_description = batch_result.failure_description();
-      if (attempt > 0) {
-        FUZZTEST_LOG(INFO) << "Crash reproduced on attempt " << (attempt + 1)
-                           << " of " << max_attempts << " for " << input_path;
+    if (stop_condition.ShouldStop()) break;
+    bool any_remaining = false;
+    for (CrashToReplay& crash : crashes) {
+      if (stop_condition.ShouldStop()) break;
+      if (!crash.new_signature.empty()) continue;
+      BatchResult batch_result;
+      if (!callbacks.Execute(env.binary, {crash.input_bytes}, batch_result) &&
+          batch_result.IsInputFailure()) {
+        crash.new_signature = batch_result.failure_signature();
+        crash.details.description = batch_result.failure_description();
+        if (attempt > 0) {
+          FUZZTEST_LOG(INFO)
+              << "Crash reproduced on attempt " << (attempt + 1) << " of "
+              << max_attempts << " for " << crash.details.input_path;
+        }
+      } else {
+        any_remaining = true;
+        if (attempt + 1 == max_attempts && max_attempts > 1) {
+          FUZZTEST_LOG(INFO)
+              << "Crash failed to reproduce after " << max_attempts
+              << " attempts for " << crash.details.input_path;
+        }
       }
-      return absl::OkStatus();
     }
-  }
-
-  if (max_attempts > 1) {
-    FUZZTEST_LOG(INFO) << "Crash failed to reproduce after " << max_attempts
-                       << " attempts for " << input_path;
-  }
-  out_signature = "";
-  out_description = "";
-  return absl::OkStatus();
-}
-
-absl::Status ReplayExistingCrashes(
-    CentipedeCallbacks& callbacks, const Environment& env,
-    std::vector<ExistingCrash>& existing_crashes) {
-  for (auto& existing : existing_crashes) {
-    if (existing.crash_report.signature.empty()) {
-      continue;
-    }
-    RETURN_IF_NOT_OK(ReplayCrash(
-        callbacks, env, existing.crash_report.details.input_path,
-        existing.new_signature, existing.crash_report.details.description));
-  }
-  return absl::OkStatus();
-}
-
-absl::Status ReplayIncubatingCrashes(
-    CentipedeCallbacks& callbacks, const Environment& env,
-    std::vector<IncubatingCrash>& incubating_crashes) {
-  for (auto& incubating : incubating_crashes) {
-    RETURN_IF_NOT_OK(ReplayCrash(callbacks, env, incubating.details.input_path,
-                                 incubating.new_signature,
-                                 incubating.details.description));
+    if (!any_remaining) break;
   }
   return absl::OkStatus();
 }
@@ -620,10 +634,18 @@
 
   ScopedCentipedeCallbacks scoped_callbacks(callbacks_factory, env,
                                             stop_condition);
-  RETURN_IF_NOT_OK(ReplayExistingCrashes(*scoped_callbacks.callbacks(), env,
-                                         existing_crashes));
-  RETURN_IF_NOT_OK(ReplayIncubatingCrashes(*scoped_callbacks.callbacks(), env,
-                                           incubating_crashes));
+  ASSIGN_OR_RETURN_IF_NOT_OK(
+      std::vector<CrashToReplay> existing_to_replay,
+      ToCrashesToReplay(absl::MakeSpan(existing_crashes)));
+  RETURN_IF_NOT_OK(ReplayCrashes(*scoped_callbacks.callbacks(), env,
+                                 stop_condition,
+                                 absl::MakeSpan(existing_to_replay)));
+  ASSIGN_OR_RETURN_IF_NOT_OK(
+      std::vector<CrashToReplay> incubating_to_replay,
+      ToCrashesToReplay(absl::MakeSpan(incubating_crashes)));
+  RETURN_IF_NOT_OK(ReplayCrashes(*scoped_callbacks.callbacks(), env,
+                                 stop_condition,
+                                 absl::MakeSpan(incubating_to_replay)));
 
   absl::flat_hash_map<std::string, CrashDetails> new_crashes = FindNewCrashes(
       new_crashes_by_signature, incubating_crashes, existing_crashes);
diff --git a/centipede/crash_deduplication_test.cc b/centipede/crash_deduplication_test.cc
index b65f557..9538aa5 100644
--- a/centipede/crash_deduplication_test.cc
+++ b/centipede/crash_deduplication_test.cc
@@ -228,6 +228,7 @@
   }
   const Environment& env() const { return env_; }
   CrashSummary& crash_summary() { return crash_summary_; }
+  StopCondition& stop_condition() { return stop_condition_; }
 
   absl::Status OrganizeCrashingInputs(
       const std::filesystem::path& regression_dir,
@@ -1190,5 +1191,101 @@
               HasSubstr("Crash failed to reproduce after 3 attempts for "));
 }
 
+class RecordingCrashCallbacks : public CentipedeCallbacks {
+ public:
+  RecordingCrashCallbacks(
+      const Environment& env, StopCondition& stop_condition,
+      absl::flat_hash_map<std::string, int> crash_on_input_attempt,
+      int stop_after_total_executions = -1)
+      : CentipedeCallbacks(env, stop_condition),
+        crash_on_input_attempt_(std::move(crash_on_input_attempt)),
+        stop_after_total_executions_(stop_after_total_executions) {}
+
+  bool Execute(std::string_view binary, absl::Span<const ByteSpan> inputs,
+               BatchResult& batch_result) override {
+    batch_result.ClearAndResize(inputs.size());
+    std::string input_str(AsStringView(inputs[0]));
+    executed_inputs_.push_back(input_str);
+    const int attempt = ++attempts_by_input_[input_str];
+    if (stop_after_total_executions_ > 0 &&
+        static_cast<int>(executed_inputs_.size()) >=
+            stop_after_total_executions_) {
+      stop_condition_.SetStopTime(absl::InfinitePast());
+    }
+    auto it = crash_on_input_attempt_.find(input_str);
+    if (it != crash_on_input_attempt_.end() && attempt == it->second) {
+      batch_result.exit_code() = EXIT_FAILURE;
+      batch_result.failure_signature() = "csig_" + input_str;
+      batch_result.failure_description() = "desc_" + input_str;
+      return false;
+    }
+    return true;
+  }
+
+  const std::vector<std::string>& executed_inputs() const {
+    return executed_inputs_;
+  }
+
+ private:
+  absl::flat_hash_map<std::string, int> crash_on_input_attempt_;
+  int stop_after_total_executions_;
+  absl::flat_hash_map<std::string, int> attempts_by_input_;
+  std::vector<std::string> executed_inputs_;
+};
+
+TEST_F(OrganizeCrashingInputsTest, ReplaysMultipleCrashesInRoundRobinOrder) {
+  SetContentsAndGetPath(crashing_dir(), "bug1-csig_input1-isig1", "input1");
+  SetContentsAndGetPath(crashing_dir(), "bug2-csig_input2-isig2", "input2");
+
+  Environment test_env = env();
+  test_env.replay_crash_attempts = 3;
+
+  // input2 reproduces on its 2nd attempt; input1 never reproduces.
+  RecordingCrashCallbacks callbacks(test_env, stop_condition(),
+                                    /*crash_on_input_attempt=*/{{"input2", 2}});
+  NonOwningCallbacksFactory factory(callbacks);
+
+  ASSERT_TRUE(OrganizeCrashingInputs(regression_dir(), crashing_dir(), test_env,
+                                     factory, /*new_crashes_by_signature=*/{},
+                                     crash_summary())
+                  .ok());
+
+  const auto& executed = callbacks.executed_inputs();
+  ASSERT_EQ(executed.size(), 5);
+  EXPECT_THAT(absl::MakeConstSpan(executed).subspan(0, 2),
+              UnorderedElementsAre("input1", "input2"));
+  EXPECT_THAT(absl::MakeConstSpan(executed).subspan(2, 2),
+              UnorderedElementsAre("input1", "input2"));
+  EXPECT_EQ(executed[4], "input1");
+}
+
+TEST_F(OrganizeCrashingInputsTest, StopsReplayingWhenStopConditionIsTriggered) {
+  SetContentsAndGetPath(crashing_dir(), "bug1-csig_input1-isig1", "input1");
+  SetContentsAndGetPath(crashing_dir(), "bug2-csig_input2-isig2", "input2");
+
+  Environment test_env = env();
+  test_env.replay_crash_attempts = 5;
+
+  // Trigger stop_condition after 2 total executions (1 pass over the 2 inputs).
+  RecordingCrashCallbacks callbacks(test_env, stop_condition(),
+                                    /*crash_on_input_attempt=*/{{"input2", 1}},
+                                    /*stop_after_total_executions=*/2);
+  NonOwningCallbacksFactory factory(callbacks);
+
+  ASSERT_TRUE(OrganizeCrashingInputs(regression_dir(), crashing_dir(), test_env,
+                                     factory, /*new_crashes_by_signature=*/{},
+                                     crash_summary())
+                  .ok());
+
+  // Only the first pass (2 executions) should run before stop_condition halts
+  // further retries, and input2 (which reproduced on pass 1) is still reported.
+  EXPECT_EQ(callbacks.executed_inputs().size(), 2);
+  std::string crash_report;
+  crash_summary().Report(&crash_report);
+  EXPECT_THAT(crash_report,
+              AllOf(HasSubstr("Total crashes: 1"),
+                    HasSubstr("Crash ID   : bug2-csig_input2-isig2")));
+}
+
 }  // namespace
 }  // namespace fuzztest::internal