Replay/export the crashing inputs in the corpus database using Centipede.

This introduces two internal Environment actions in Centipede: `replay_crash` and `export_crash` with options `crash_id` and `export_crash_file` to instruct Centipede to replay a single crash, or export the crash for replaying in a single process.

PiperOrigin-RevId: 740855501
diff --git a/centipede/centipede_interface.cc b/centipede/centipede_interface.cc
index 3009e72..e263c84 100644
--- a/centipede/centipede_interface.cc
+++ b/centipede/centipede_interface.cc
@@ -694,6 +694,70 @@
   return EXIT_SUCCESS;
 }
 
+int ReplayCrash(const Environment &env,
+                const fuzztest::internal::Configuration &target_config,
+                CentipedeCallbacksFactory &callbacks_factory) {
+  CHECK(!env.crash_id.empty()) << "Need crash_id to be set for replay a crash";
+  CHECK(target_config.fuzz_tests_in_current_shard.size() == 1)
+      << "Expecting exactly one test for replay_crash";
+  // TODO: b/406003594 - move the path construction to a libarary.
+  const auto crash_dir = std::filesystem::path(target_config.corpus_database) /
+                         target_config.binary_identifier /
+                         target_config.fuzz_tests_in_current_shard[0] /
+                         "crashing";
+  const WorkDir workdir{env};
+  SeedCorpusSource crash_corpus_source;
+  crash_corpus_source.dir_glob = crash_dir;
+  crash_corpus_source.num_recent_dirs = 1;
+  crash_corpus_source.individual_input_rel_glob = env.crash_id;
+  crash_corpus_source.sampled_fraction_or_count = 1.0f;
+  const SeedCorpusConfig crash_corpus_config = {
+      /*sources=*/{crash_corpus_source},
+      /*destination=*/{
+          /*dir_path=*/env.workdir,
+          /*shard_rel_glob=*/
+          std::filesystem::path{workdir.CorpusFilePaths().AllShardsGlob()}
+              .filename(),
+          /*shard_index_digits=*/WorkDir::kDigitsInShardIndex,
+          /*num_shards=*/1}};
+  CHECK_OK(GenerateSeedCorpusFromConfig(crash_corpus_config, env.binary_name,
+                                        env.binary_hash));
+  Environment run_crash_env = env;
+  run_crash_env.load_shards_only = true;
+  return Fuzz(run_crash_env, {}, "", callbacks_factory);
+}
+
+int ExportCrash(const Environment &env,
+                const fuzztest::internal::Configuration &target_config) {
+  CHECK(!env.crash_id.empty())
+      << "Need crash_id to be set for exporting a crash";
+  CHECK(!env.export_crash_file.empty())
+      << "Need export_crash_file to be set for exporting a crash";
+  CHECK(target_config.fuzz_tests_in_current_shard.size() == 1)
+      << "Expecting exactly one test for exporting a crash";
+  // TODO: b/406003594 - move the path construction to a libarary.
+  const auto crash_dir = std::filesystem::path(target_config.corpus_database) /
+                         target_config.binary_identifier /
+                         target_config.fuzz_tests_in_current_shard[0] /
+                         "crashing";
+  std::string crash_contents;
+  const auto read_status =
+      RemoteFileGetContents((crash_dir / env.crash_id).c_str(), crash_contents);
+  if (!read_status.ok()) {
+    LOG(ERROR) << "Failed reading the crash " << env.crash_id << " from "
+               << crash_dir.c_str() << ": " << read_status;
+    return EXIT_FAILURE;
+  }
+  const auto write_status =
+      RemoteFileSetContents(env.export_crash_file, crash_contents);
+  if (!write_status.ok()) {
+    LOG(ERROR) << "Failed write the crash " << env.crash_id << " to "
+               << env.export_crash_file << ": " << write_status;
+    return EXIT_FAILURE;
+  }
+  return EXIT_SUCCESS;
+}
+
 }  // namespace
 
 int CentipedeMain(const Environment &env,
@@ -755,6 +819,15 @@
       CHECK_OK(target_config.status())
           << "Failed to deserialize target configuration";
       if (!target_config->corpus_database.empty()) {
+        CHECK(!env.replay_crash || !env.export_crash)
+            << "replay_crash and export_crash cannot be both set";
+        if (env.replay_crash) {
+          return ReplayCrash(env, *target_config, callbacks_factory);
+        }
+        if (env.export_crash) {
+          return ExportCrash(env, *target_config);
+        }
+
         const auto time_limit_per_test = target_config->GetTimeLimitPerTest();
         CHECK(target_config->only_replay ||
               time_limit_per_test < absl::InfiniteDuration())
diff --git a/centipede/environment.h b/centipede/environment.h
index 86fe54f..93f8836 100644
--- a/centipede/environment.h
+++ b/centipede/environment.h
@@ -135,6 +135,14 @@
   // If set, deserializes the configuration from the value instead of querying
   // the configuration via runner callbacks.
   std::string fuzztest_configuration;
+  // The crash ID used for `replay_crash` or `export_crash`.
+  std::string crash_id;
+  // If set, replay `crash_id` in the corpus database.
+  bool replay_crash = false;
+  // If set, export the input contents of `crash_id` from the corpus database.
+  bool export_crash = false;
+  // The path to export the input contents of `crash_id` for `export_crash`.
+  std::string export_crash_file;
 
   // Command line-related fields -----------------------------------------------
 
diff --git a/centipede/environment_flags.cc b/centipede/environment_flags.cc
index a650077..d3d1c86 100644
--- a/centipede/environment_flags.cc
+++ b/centipede/environment_flags.cc
@@ -536,6 +536,10 @@
       /*fuzztest_single_test_mode=*/
       Environment::Default().fuzztest_single_test_mode,
       /*fuzztest_configuration=*/Environment::Default().fuzztest_configuration,
+      /*crash_id=*/Environment::Default().crash_id,
+      /*replay_crash=*/Environment::Default().replay_crash,
+      /*export_crash=*/Environment::Default().export_crash,
+      /*export_crash_file=*/Environment::Default().export_crash_file,
       /*exec_name=*/Environment::Default().exec_name,
       /*args=*/Environment::Default().args,
       /*binary_name=*/
diff --git a/fuzztest/init_fuzztest.cc b/fuzztest/init_fuzztest.cc
index da0a717..423f70e 100644
--- a/fuzztest/init_fuzztest.cc
+++ b/fuzztest/init_fuzztest.cc
@@ -176,6 +176,8 @@
 //
 // These flags are meant to be set only by the parent controller process for its
 // child processes.
+//
+// TODO(b/406001082): Remove these flags once they are no longer needed.
 
 FUZZTEST_DEFINE_FLAG(
     std::optional<std::string>, internal_override_fuzz_test, std::nullopt,
@@ -206,6 +208,21 @@
           "internal_override_total_time_limit directly");
     });
 
+FUZZTEST_DEFINE_FLAG(std::optional<std::string>,
+                     internal_crashing_input_to_reproduce, std::nullopt,
+                     "Internal-only flag - do not use directly. If both this "
+                     "and --" FUZZTEST_FLAG_PREFIX
+                     "internal_override_fuzz_test are set, replay "
+                     "the input in the corpus database with the specified ID.")
+    .OnUpdate([] {
+      FUZZTEST_INTERNAL_CHECK_PRECONDITION(
+          !absl::GetFlag(FUZZTEST_FLAG(internal_crashing_input_to_reproduce))
+                  .has_value() ||
+              std::getenv("CENTIPEDE_RUNNER_FLAGS") != nullptr,
+          "must not set --" FUZZTEST_FLAG_PREFIX
+          "internal_crashing_input_to_reproduce directly");
+    });
+
 namespace fuzztest {
 
 std::vector<std::string> ListRegisteredTests() {
@@ -314,12 +331,14 @@
       reproduce_findings_as_separate_tests, replay_coverage_inputs,
       /*only_replay=*/
       replay_corpus_time_limit.has_value(),
+      /*replay_in_single_process=*/false,
       absl::GetFlag(FUZZTEST_FLAG(execution_id)),
       absl::GetFlag(FUZZTEST_FLAG(print_subprocess_log)),
       /*stack_limit=*/absl::GetFlag(FUZZTEST_FLAG(stack_limit_kb)) * 1024,
       /*rss_limit=*/absl::GetFlag(FUZZTEST_FLAG(rss_limit_mb)) * 1024 * 1024,
       absl::GetFlag(FUZZTEST_FLAG(time_limit_per_input)), time_limit,
-      time_budget_type, jobs.value_or(0)};
+      time_budget_type, jobs.value_or(0),
+      absl::GetFlag(FUZZTEST_FLAG(internal_crashing_input_to_reproduce))};
 }
 }  // namespace
 
diff --git a/fuzztest/internal/centipede_adaptor.cc b/fuzztest/internal/centipede_adaptor.cc
index 7f89758..638fca5 100644
--- a/fuzztest/internal/centipede_adaptor.cc
+++ b/fuzztest/internal/centipede_adaptor.cc
@@ -239,6 +239,16 @@
                   " --" FUZZTEST_FLAG_PREFIX
                   "internal_override_total_time_limit=",
                   total_time_limit);
+  if (configuration.crashing_input_to_reproduce.has_value()) {
+    absl::StrAppend(&env.binary,
+                    " --" FUZZTEST_FLAG_PREFIX
+                    "internal_crashing_input_to_reproduce=",
+                    *configuration.crashing_input_to_reproduce);
+    env.crash_id =
+        std::filesystem::path(*configuration.crashing_input_to_reproduce)
+            .filename();
+    env.replay_crash = true;
+  }
   env.coverage_binary = (*args)[0];
   env.binary_name = std::filesystem::path{(*args)[0]}.filename();
   env.binary_hash = GetSelfBinaryHashForCentipedeEnvironment();
@@ -586,7 +596,9 @@
   // and we should not run CentipedeMain in this process.
   const bool runner_mode = std::getenv("CENTIPEDE_RUNNER_FLAGS");
   const bool is_running_property_function_in_this_process =
-      runner_mode || configuration.crashing_input_to_reproduce.has_value() ||
+      runner_mode ||
+      (configuration.crashing_input_to_reproduce.has_value() &&
+       configuration.replay_in_single_process) ||
       std::getenv("FUZZTEST_REPLAY") ||
       std::getenv("FUZZTEST_MINIMIZE_REPRODUCER");
   if (!is_running_property_function_in_this_process &&
@@ -627,7 +639,10 @@
     // Centipede engine does not support replay and reproducer minimization
     // (within the single process). So use the existing fuzztest implementation.
     // This is fine because it does not require coverage instrumentation.
-    if (fuzzer_impl_.ReplayInputsIfAvailable(configuration)) return 0;
+    if (!configuration.crashing_input_to_reproduce.has_value() &&
+        fuzzer_impl_.ReplayInputsIfAvailable(configuration)) {
+      return 0;
+    }
     // `ReplayInputsIfAvailable` overwrites the run mode - revert it back.
     runtime_.SetRunMode(mode);
     // Tear down fixture early to avoid interfering with the runners.
@@ -643,10 +658,31 @@
         configuration, workdir_path, test_.full_name(), mode);
     centipede::DefaultCallbacksFactory<centipede::CentipedeDefaultCallbacks>
         factory;
-    if (const char* minimize_dir_chars =
-            std::getenv("FUZZTEST_MINIMIZE_TESTSUITE_DIR");
-        configuration.corpus_database.empty() &&
-        minimize_dir_chars != nullptr) {
+    if (!configuration.corpus_database.empty()) {
+      if (!env.crash_id.empty() && configuration.replay_in_single_process) {
+        TempDir crash_fetch_dir("fuzztest_crash");
+        auto export_crash_env = env;
+        std::string crash_file =
+            (std::filesystem::path(crash_fetch_dir.path()) / "crash").string();
+        export_crash_env.export_crash_file = crash_file;
+        export_crash_env.replay_crash = false;
+        export_crash_env.export_crash = true;
+        if (centipede::CentipedeMain(export_crash_env, factory) !=
+            EXIT_SUCCESS) {
+          absl::FPrintF(
+              GetStderr(),
+              "[!] Encountered error when using Centipede to export the crash "
+              "input.");
+          return EXIT_FAILURE;
+        }
+        CentipedeAdaptorRunnerCallbacks runner_callbacks(
+            &runtime_, &fuzzer_impl_, &configuration);
+        static char replay_argv0[] = "replay_argv";
+        char* replay_argv[] = {replay_argv0, crash_file.data()};
+        return centipede::RunnerMain(/*argc=*/2, replay_argv, runner_callbacks);
+      }
+    } else if (const char* minimize_dir_chars =
+                   std::getenv("FUZZTEST_MINIMIZE_TESTSUITE_DIR")) {
       const std::string minimize_dir = minimize_dir_chars;
       const char* corpus_out_dir_chars =
           std::getenv("FUZZTEST_TESTSUITE_OUT_DIR");
diff --git a/fuzztest/internal/configuration.cc b/fuzztest/internal/configuration.cc
index ccb933c..40b64f3 100644
--- a/fuzztest/internal/configuration.cc
+++ b/fuzztest/internal/configuration.cc
@@ -207,11 +207,11 @@
              SpaceFor(fuzz_tests) + SpaceFor(fuzz_tests_in_current_shard) +
              SpaceFor(reproduce_findings_as_separate_tests) +
              SpaceFor(replay_coverage_inputs) + SpaceFor(only_replay) +
-             SpaceFor(execution_id) + SpaceFor(print_subprocess_log) +
-             SpaceFor(stack_limit) + SpaceFor(rss_limit) +
-             SpaceFor(time_limit_per_input_str) + SpaceFor(time_limit_str) +
-             SpaceFor(time_budget_type_str) + SpaceFor(jobs) +
-             SpaceFor(crashing_input_to_reproduce) +
+             SpaceFor(replay_in_single_process) + SpaceFor(execution_id) +
+             SpaceFor(print_subprocess_log) + SpaceFor(stack_limit) +
+             SpaceFor(rss_limit) + SpaceFor(time_limit_per_input_str) +
+             SpaceFor(time_limit_str) + SpaceFor(time_budget_type_str) +
+             SpaceFor(jobs) + SpaceFor(crashing_input_to_reproduce) +
              SpaceFor(reproduction_command_template));
   size_t offset = 0;
   offset = WriteString(out, offset, corpus_database);
@@ -223,6 +223,7 @@
   offset = WriteIntegral(out, offset, reproduce_findings_as_separate_tests);
   offset = WriteIntegral(out, offset, replay_coverage_inputs);
   offset = WriteIntegral(out, offset, only_replay);
+  offset = WriteIntegral(out, offset, replay_in_single_process);
   offset = WriteOptionalString(out, offset, execution_id);
   offset = WriteIntegral(out, offset, print_subprocess_log);
   offset = WriteIntegral(out, offset, stack_limit);
@@ -251,6 +252,7 @@
                      Consume<bool>(serialized));
     ASSIGN_OR_RETURN(replay_coverage_inputs, Consume<bool>(serialized));
     ASSIGN_OR_RETURN(only_replay, Consume<bool>(serialized));
+    ASSIGN_OR_RETURN(replay_in_single_process, Consume<bool>(serialized));
     ASSIGN_OR_RETURN(execution_id, ConsumeOptionalString(serialized));
     ASSIGN_OR_RETURN(print_subprocess_log, Consume<bool>(serialized));
     ASSIGN_OR_RETURN(stack_limit, Consume<size_t>(serialized));
@@ -281,6 +283,7 @@
                          *reproduce_findings_as_separate_tests,
                          *replay_coverage_inputs,
                          *only_replay,
+                         *replay_in_single_process,
                          *std::move(execution_id),
                          *print_subprocess_log,
                          *stack_limit,
diff --git a/fuzztest/internal/configuration.h b/fuzztest/internal/configuration.h
index fed9518..3c93ec8 100644
--- a/fuzztest/internal/configuration.h
+++ b/fuzztest/internal/configuration.h
@@ -74,6 +74,8 @@
   bool replay_coverage_inputs = false;
   // If set, further steps are skipped after replaying.
   bool only_replay = false;
+  // If set, replay without spawning subprocesses.
+  bool replay_in_single_process = false;
   // If set, will be used when working on a corpus database to resume
   // the progress in case the execution got interrupted.
   std::optional<std::string> execution_id;
diff --git a/fuzztest/internal/configuration_test.cc b/fuzztest/internal/configuration_test.cc
index 16982e9..f5475d6 100644
--- a/fuzztest/internal/configuration_test.cc
+++ b/fuzztest/internal/configuration_test.cc
@@ -24,6 +24,7 @@
              other->reproduce_findings_as_separate_tests &&
          config.replay_coverage_inputs == other->replay_coverage_inputs &&
          config.only_replay == other->only_replay &&
+         config.replay_in_single_process == other->replay_in_single_process &&
          config.execution_id == other->execution_id &&
          config.print_subprocess_log == other->print_subprocess_log &&
          config.stack_limit == other->stack_limit &&
@@ -49,6 +50,7 @@
                               /*reproduce_findings_as_separate_tests=*/true,
                               /*replay_coverage_inputs=*/true,
                               /*only_replay=*/true,
+                              /*replay_in_single_process=*/true,
                               "execution_id",
                               /*print_subprocess_log=*/true,
                               /*stack_limit=*/100,
@@ -75,6 +77,7 @@
                               /*reproduce_findings_as_separate_tests=*/true,
                               /*replay_coverage_inputs=*/true,
                               /*only_replay=*/true,
+                              /*replay_in_single_process=*/true,
                               "execution_id",
                               /*print_subprocess_log=*/true,
                               /*stack_limit=*/100,
diff --git a/fuzztest/internal/googletest_adaptor.h b/fuzztest/internal/googletest_adaptor.h
index 3e7f2d4..d2b2406 100644
--- a/fuzztest/internal/googletest_adaptor.h
+++ b/fuzztest/internal/googletest_adaptor.h
@@ -40,20 +40,20 @@
   void TestBody() override {
     auto test = test_.make();
     configuration_.fuzz_tests_in_current_shard = GetFuzzTestsInCurrentShard();
+    configuration_.replay_in_single_process =
+        configuration_.crashing_input_to_reproduce.has_value() &&
+        testing::UnitTest::GetInstance()->test_to_run_count() == 1;
     if (Runtime::instance().run_mode() == RunMode::kUnitTest) {
       // In "bug reproduction" mode, sometimes we need to reproduce multiple
       // bugs, i.e., run multiple tests that lead to a crash.
       bool needs_subprocess = false;
-#ifdef GTEST_HAS_DEATH_TEST
+#if defined(GTEST_HAS_DEATH_TEST) && !defined(FUZZTEST_USE_CENTIPEDE)
       needs_subprocess =
           configuration_.crashing_input_to_reproduce.has_value() &&
-          (
-              // When only a single test runs, it's okay to crash the process on
-              // error, as we don't need to run other tests.
-              testing::UnitTest::GetInstance()->test_to_run_count() > 1 ||
-              // EXPECT_EXIT is required in the death-test subprocess, but in
-              // the subprocess there's only one test to run.
-              testing::internal::InDeathTestChild());
+          (!configuration_.replay_in_single_process ||
+           // EXPECT_EXIT is required in the death-test subprocess, but in
+           // the subprocess there's only one test to run.
+           testing::internal::InDeathTestChild());
 #endif
       if (needs_subprocess) {
         configuration_.preprocess_crash_reproducing = [] {