Make MinimizeCrash use replay_crash_attempts.

PiperOrigin-RevId: 998796541
diff --git a/centipede/BUILD b/centipede/BUILD
index 5e1690e..e5675e4 100644
--- a/centipede/BUILD
+++ b/centipede/BUILD
@@ -384,6 +384,7 @@
         "@abseil-cpp//absl/base:core_headers",
         "@abseil-cpp//absl/container:flat_hash_set",
         "@abseil-cpp//absl/synchronization",
+        "@abseil-cpp//absl/types:span",
         "@com_google_fuzztest//common:defs",
         "@com_google_fuzztest//common:logging",
     ],
diff --git a/centipede/centipede_interface.cc b/centipede/centipede_interface.cc
index fea652e..ee22718 100644
--- a/centipede/centipede_interface.cc
+++ b/centipede/centipede_interface.cc
@@ -123,7 +123,9 @@
     ScopedCentipedeCallbacks scoped_callbacks(callbacks_factory, env,
                                               stop_condition);
     CreateLocalDirRemovedAtExit(TemporaryLocalDirPath());
-    if (scoped_callbacks.callbacks()->Execute(env.binary, {crashy_input},
+    const std::vector<ByteSpan> inputs(
+        std::max<size_t>(1, env.replay_crash_attempts), crashy_input);
+    if (scoped_callbacks.callbacks()->Execute(env.binary, inputs,
                                               batch_result)) {
       FUZZTEST_LOG(ERROR) << "The original crashy input did not crash; exiting";
       stop_condition.RequestStop(EXIT_FAILURE,
diff --git a/centipede/minimize_crash.cc b/centipede/minimize_crash.cc
index 66ef9a5..d7feba5 100644
--- a/centipede/minimize_crash.cc
+++ b/centipede/minimize_crash.cc
@@ -26,6 +26,7 @@
 #include "absl/base/thread_annotations.h"
 #include "absl/container/flat_hash_set.h"
 #include "absl/synchronization/mutex.h"
+#include "absl/types/span.h"
 #include "./centipede/centipede_callbacks.h"
 #include "./centipede/environment.h"
 #include "./centipede/mutation_data.h"
@@ -89,6 +90,7 @@
   auto callbacks = scoped_callback.callbacks();
   BatchResult batch_result;
 
+  const size_t max_attempts = std::max<size_t>(1, env.replay_crash_attempts);
   size_t num_batches = env.num_runs / env.batch_size;
   for (size_t i = 0; i < num_batches; ++i) {
     if (stop_condition.ShouldStop()) break;
@@ -127,22 +129,38 @@
     std::sort(smaller_mutants.begin(), smaller_mutants.end(),
               [](const auto& a, const auto& b) { return a.size() < b.size(); });
 
-    // Execute all mutants. If a new crasher is found, add it to `state`.
-    if (callbacks->Execute(env.binary, smaller_mutants, batch_result)) {
-      continue;
+    std::vector<ByteSpan> inputs_to_execute;
+    inputs_to_execute.reserve(smaller_mutants.size() * max_attempts);
+    for (const auto& m : smaller_mutants) {
+      inputs_to_execute.insert(inputs_to_execute.end(), max_attempts, m);
     }
 
-    if (batch_result.failure_signature() != crash_signature) {
-      continue;
-    }
+    // Execute all mutants in chunks of at most `env.batch_size`. If a new
+    // crasher is found, add it to `state`.
+    for (size_t offset = 0; offset < inputs_to_execute.size();
+         offset += env.batch_size) {
+      if (stop_condition.ShouldStop()) break;
+      const size_t chunk_size =
+          std::min(inputs_to_execute.size() - offset, env.batch_size);
+      const absl::Span<const ByteSpan> chunk =
+          absl::MakeConstSpan(inputs_to_execute).subspan(offset, chunk_size);
+      if (callbacks->Execute(env.binary, chunk, batch_result)) {
+        continue;
+      }
 
-    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];
-    FUZZTEST_LOG(INFO) << "Crasher: size: " << new_crasher.size() << ": "
-                       << AsPrintableString(new_crasher, /*max_len=*/40);
-    state.AddCrasher({new_crasher.begin(), new_crasher.end()},
-                     std::move(batch_result.failure_description()));
+      if (batch_result.failure_signature() != crash_signature) {
+        continue;
+      }
+
+      size_t crash_inputs_idx = batch_result.num_outputs_read();
+      FUZZTEST_CHECK_LT(crash_inputs_idx, chunk.size());
+      const auto& new_crasher = chunk[crash_inputs_idx];
+      FUZZTEST_LOG(INFO) << "Crasher: size: " << new_crasher.size() << ": "
+                         << AsPrintableString(new_crasher, /*max_len=*/40);
+      state.AddCrasher({new_crasher.begin(), new_crasher.end()},
+                       std::move(batch_result.failure_description()));
+      break;
+    }
   }
 }
 
diff --git a/centipede/minimize_crash_test.cc b/centipede/minimize_crash_test.cc
index 5b27047..6920065 100644
--- a/centipede/minimize_crash_test.cc
+++ b/centipede/minimize_crash_test.cc
@@ -14,6 +14,7 @@
 
 #include "./centipede/minimize_crash.h"
 
+#include <cstddef>
 #include <cstdlib>
 #include <string>
 #include <string_view>
@@ -40,15 +41,27 @@
 // A mock for CentipedeCallbacks.
 class MinimizerMock : public CentipedeCallbacks {
  public:
-  MinimizerMock(const Environment& env, StopCondition& stop_condition)
-      : CentipedeCallbacks(env, stop_condition) {}
+  MinimizerMock(const Environment& env, StopCondition& stop_condition,
+                size_t crash_on_attempt = 1)
+      : CentipedeCallbacks(env, stop_condition),
+        crash_on_attempt_(crash_on_attempt) {}
 
-  // Runs FuzzMe() on every input, imitates failure if FuzzMe() returns true.
+  // Runs FuzzMe() on every input, imitates failure if FuzzMe() returns true
+  // `crash_on_attempt_` consecutive times on the same input.
   bool Execute(std::string_view binary, absl::Span<const ByteSpan> inputs,
                BatchResult& batch_result) override {
+    EXPECT_LE(inputs.size(), env_.batch_size);
     batch_result.ClearAndResize(inputs.size());
-    for (auto input : inputs) {
-      if (FuzzMe(input)) {
+    for (size_t i = 0; i < inputs.size(); ++i) {
+      const auto& input = inputs[i];
+      if (input.data() == last_input_.data() &&
+          input.size() == last_input_.size()) {
+        ++current_attempts_;
+      } else {
+        last_input_ = input;
+        current_attempts_ = 1;
+      }
+      if (FuzzMe(input) && current_attempts_ >= crash_on_attempt_) {
         batch_result.exit_code() = EXIT_FAILURE;
         // Set signature differently to test signature matching behavior.
         batch_result.failure_signature() =
@@ -73,16 +86,26 @@
     }
     return true;
   }
+
+  size_t crash_on_attempt_ = 1;
+  ByteSpan last_input_;
+  size_t current_attempts_ = 0;
 };
 
 // Factory that creates/destroys MinimizerMock.
 class MinimizerMockFactory : public CentipedeCallbacksFactory {
  public:
+  explicit MinimizerMockFactory(size_t crash_on_attempt = 1)
+      : crash_on_attempt_(crash_on_attempt) {}
+
   CentipedeCallbacks* absl_nonnull create(
       const Environment& env, StopCondition& stop_condition) override {
-    return new MinimizerMock(env, stop_condition);
+    return new MinimizerMock(env, stop_condition, crash_on_attempt_);
   }
   void destroy(CentipedeCallbacks *cb) override { delete cb; }
+
+ private:
+  size_t crash_on_attempt_ = 1;
 };
 
 TEST(MinimizeTest, FailsWhenCrasherCannotBeMinimized) {
@@ -154,5 +177,27 @@
   EXPECT_THAT(minimized_crashers, UnorderedElementsAre(ByteArray{'z'}));
 }
 
+TEST(MinimizeTest, MinimizesWithReplayCrashAttempts) {
+  Environment env;
+  env.num_runs = 100000;
+  MinimizerMockFactory factory(/*crash_on_attempt=*/3);
+  StopCondition stop_condition;
+
+  ByteArray original_crasher = {'f', 'f', 'f', 'f', 'f', 'f',
+                                'z', 'z', 'z', 'z', 'z', 'z'};
+  env.replay_crash_attempts = 2;
+  EXPECT_FALSE(MinimizeCrash(original_crasher, env, factory, "signature one",
+                             stop_condition)
+                   .has_value());
+
+  env.replay_crash_attempts = 3;
+  auto result = MinimizeCrash(original_crasher, env, factory, "signature one",
+                              stop_condition);
+  ASSERT_TRUE(result.has_value());
+  EXPECT_EQ(result->input, ByteArray{'f'});
+  EXPECT_EQ(result->description, "description one");
+  EXPECT_FALSE(stop_condition.StopRequested());
+}
+
 }  // namespace
 }  // namespace fuzztest::internal