No public description

PiperOrigin-RevId: 955986630
diff --git a/centipede/centipede_flags.inc b/centipede/centipede_flags.inc
index 87a8e60..5d91f66 100644
--- a/centipede/centipede_flags.inc
+++ b/centipede/centipede_flags.inc
@@ -509,6 +509,12 @@
                "If set, further steps are skipped after replaying with the "
                "corpus database.")
 CENTIPEDE_FLAG(
+    std::optional<bool>, fuzztest_update_corpus_database, std::nullopt,
+    "If true, update the corpus database (both crashes and coverage). "
+    "If false, do not update it. "
+    "If unspecified, it defaults to true in fuzzing mode and false "
+    "in replay mode.")
+CENTIPEDE_FLAG(
     std::string, fuzztest_execution_id, "",
     "If set, will be used when working on a corpus database to resume "
     "the progress in case the execution got interrupted.")
diff --git a/centipede/centipede_interface.cc b/centipede/centipede_interface.cc
index 489bd97..6641093 100644
--- a/centipede/centipede_interface.cc
+++ b/centipede/centipede_interface.cc
@@ -335,6 +335,90 @@
           PeriodicAction::ZeroDelayConstInterval(absl::Seconds(15))};
 }
 
+struct DatabasePaths {
+  std::filesystem::path fuzztest_db_path;
+  std::filesystem::path regression_dir;
+  std::filesystem::path coverage_dir;
+  std::filesystem::path crashing_dir;
+  std::filesystem::path incubating_dir;
+};
+
+DatabasePaths GetDatabasePaths(const Environment& env) {
+  const auto corpus_database_path =
+      std::filesystem::path(env.fuzztest_corpus_database) /
+      env.fuzztest_binary_identifier;
+  const std::filesystem::path fuzztest_db_path =
+      corpus_database_path / env.test_name;
+  return {
+      fuzztest_db_path,
+      fuzztest_db_path / "regression",
+      fuzztest_db_path / "coverage",
+      fuzztest_db_path / "crashing",
+      fuzztest_db_path / "incubating",
+  };
+}
+
+void UpdateCorpus(const Environment& env, const DatabasePaths& db_paths,
+                  CentipedeCallbacksFactory& callbacks_factory,
+                  StopCondition& stop_condition) {
+  const WorkDir workdir{env};
+
+  // The test time limit does not apply for the rest of the steps.
+  stop_condition.SetStopTime(absl::InfiniteFuture());
+
+  const bool update_crashing_db =
+      env.fuzztest_update_corpus_database.value_or(!env.fuzztest_only_replay);
+  const bool update_coverage_db =
+      update_crashing_db && !env.fuzztest_only_replay;
+  if (!update_crashing_db && !env.report_crash_summary) {
+    return;
+  }
+
+  // Deduplicate and optionally update the crashing inputs.
+  CrashSummary crash_summary{env.fuzztest_binary_identifier, env.test_name};
+  const absl::flat_hash_map<std::string, CrashDetails> crashes_by_signature =
+      GetCrashesFromWorkdir(workdir, env.total_shards);
+  if (update_crashing_db) {
+    const absl::Status status = OrganizeCrashingInputs(
+        db_paths.regression_dir, db_paths.crashing_dir, db_paths.incubating_dir,
+        env, callbacks_factory, crashes_by_signature, crash_summary,
+        stop_condition, kDefaultRegressionTtl);
+    FUZZTEST_LOG_IF(ERROR, !status.ok())
+        << "Failed to organize crashing inputs: " << status;
+    if (env.report_crash_summary) crash_summary.Report(&std::cerr);
+  } else if (env.report_crash_summary) {
+    // Just report the crashes.
+    for (const auto& [crash_signature, crash_details] : crashes_by_signature) {
+      crash_summary.AddCrash({/*id=*/crash_details.input_signature,
+                              /*category=*/crash_details.description,
+                              crash_signature, crash_details.description});
+    }
+    crash_summary.Report(&std::cerr);
+  }
+
+  if (update_coverage_db) {
+    // Distill and store the coverage corpus.
+    Distill(env);
+    if (RemotePathExists(db_paths.coverage_dir.c_str())) {
+      // In the future, we will store k latest coverage corpora for some k, but
+      // for now we only keep the latest one.
+      FUZZTEST_CHECK_OK(RemotePathDelete(db_paths.coverage_dir.c_str(),
+                                         /*recursively=*/true));
+    }
+    FUZZTEST_CHECK_OK(RemoteMkdir(db_paths.coverage_dir.c_str()));
+    std::vector<std::string> distilled_corpus_files;
+    FUZZTEST_CHECK_OK(
+        RemoteGlobMatch(workdir.DistilledCorpusFilePaths().AllShardsGlob(),
+                        distilled_corpus_files));
+    for (const std::string& corpus_file : distilled_corpus_files) {
+      const std::string file_name =
+          std::filesystem::path(corpus_file).filename();
+      FUZZTEST_CHECK_OK(RemoteFileRename(
+          corpus_file, (db_paths.coverage_dir / file_name).c_str()));
+    }
+  }
+}
+
 void UpdateCorpusDatabase(Environment env,
                           CentipedeCallbacksFactory& callbacks_factory,
                           StopCondition& stop_condition) {
@@ -455,18 +539,16 @@
     }
   };
 
-  const std::filesystem::path fuzztest_db_path =
-      corpus_database_path / env.test_name;
-  const std::filesystem::path regression_dir = fuzztest_db_path / "regression";
-  const std::filesystem::path coverage_dir = fuzztest_db_path / "coverage";
+  const DatabasePaths db_paths = GetDatabasePaths(env);
 
   // Seed the fuzzing session with the latest coverage corpus and regression
   // inputs from the previous fuzzing session.
   if (!is_resuming) {
     FUZZTEST_CHECK_OK(GenerateSeedCorpusFromConfig(
-        GetSeedCorpusConfig(
-            env, regression_dir.c_str(),
-            env.fuzztest_replay_coverage_inputs ? coverage_dir.c_str() : ""),
+        GetSeedCorpusConfig(env, db_paths.regression_dir.c_str(),
+                            env.fuzztest_replay_coverage_inputs
+                                ? db_paths.coverage_dir.c_str()
+                                : ""),
         env.binary_name, env.binary_hash))
         << "while generating the seed corpus";
   }
@@ -524,57 +606,7 @@
     }
     return;
   }
-  // The test time limit does not apply for the rest of the steps.
-  stop_condition.SetStopTime(absl::InfiniteFuture());
-
-  // TODO(xinhaoyuan): Have a separate flag to skip corpus updating instead
-  // of checking whether workdir is specified or not.
-  const bool skip_corpus_db_update =
-      env.fuzztest_only_replay || is_workdir_specified;
-  if (skip_corpus_db_update && !env.report_crash_summary) return;
-
-  // Deduplicate and optionally update the crashing inputs.
-  CrashSummary crash_summary{env.fuzztest_binary_identifier, env.test_name};
-  const absl::flat_hash_map<std::string, CrashDetails> crashes_by_signature =
-      GetCrashesFromWorkdir(workdir, env.total_shards);
-  if (skip_corpus_db_update) {
-    // Just report the crashes.
-    FUZZTEST_CHECK(env.report_crash_summary);
-    for (const auto& [crash_signature, crash_details] : crashes_by_signature) {
-      crash_summary.AddCrash({/*id=*/crash_details.input_signature,
-                              /*category=*/crash_details.description,
-                              crash_signature, crash_details.description});
-    }
-    crash_summary.Report(&std::cerr);
-    return;
-  }
-  const absl::Status status = OrganizeCrashingInputs(
-      regression_dir, fuzztest_db_path / "crashing",
-      fuzztest_db_path / "incubating", env, callbacks_factory,
-      crashes_by_signature, crash_summary, stop_condition,
-      kDefaultRegressionTtl);
-  FUZZTEST_LOG_IF(ERROR, !status.ok())
-      << "Failed to organize crashing inputs: " << status;
-  if (env.report_crash_summary) crash_summary.Report(&std::cerr);
-
-  // Distill and store the coverage corpus.
-  Distill(env);
-  if (RemotePathExists(coverage_dir.c_str())) {
-    // In the future, we will store k latest coverage corpora for some k, but
-    // for now we only keep the latest one.
-    FUZZTEST_CHECK_OK(
-        RemotePathDelete(coverage_dir.c_str(), /*recursively=*/true));
-  }
-  FUZZTEST_CHECK_OK(RemoteMkdir(coverage_dir.c_str()));
-  std::vector<std::string> distilled_corpus_files;
-  FUZZTEST_CHECK_OK(
-      RemoteGlobMatch(workdir.DistilledCorpusFilePaths().AllShardsGlob(),
-                      distilled_corpus_files));
-  for (const std::string& corpus_file : distilled_corpus_files) {
-    const std::string file_name = std::filesystem::path(corpus_file).filename();
-    FUZZTEST_CHECK_OK(
-        RemoteFileRename(corpus_file, (coverage_dir / file_name).c_str()));
-  }
+  UpdateCorpus(env, db_paths, callbacks_factory, stop_condition);
 }
 
 int ListCrashIds(const Environment& env) {
@@ -583,9 +615,7 @@
   FUZZTEST_CHECK(!env.test_name.empty());
   std::vector<std::string> crash_paths;
   // TODO: b/406003594 - move the path construction to a library.
-  const auto crash_dir = std::filesystem::path(env.fuzztest_corpus_database) /
-                         env.fuzztest_binary_identifier / env.test_name /
-                         "crashing";
+  const auto crash_dir = GetDatabasePaths(env).crashing_dir;
   if (RemotePathExists(crash_dir.string())) {
     FUZZTEST_CHECK(RemotePathIsDirectory(crash_dir.string()))
         << "Crash dir " << crash_dir << " in the corpus database "
diff --git a/centipede/environment.cc b/centipede/environment.cc
index 37d2189..e45478b 100644
--- a/centipede/environment.cc
+++ b/centipede/environment.cc
@@ -246,6 +246,7 @@
   fuzztest_stats_root = config.stats_root;
   fuzztest_workdir_root = config.workdir_root;
   fuzztest_only_replay = config.only_replay;
+  fuzztest_update_corpus_database = config.update_corpus_database;
   fuzztest_execution_id = config.execution_id.value_or("");
   fuzztest_replay_coverage_inputs = config.replay_coverage_inputs;
   fuzztest_time_limit_per_test = config.GetTimeLimitPerTest();
diff --git a/centipede/environment.h b/centipede/environment.h
index 4871fc8..b30956d 100644
--- a/centipede/environment.h
+++ b/centipede/environment.h
@@ -19,6 +19,7 @@
 #include <cstddef>
 #include <cstdint>
 #include <limits>
+#include <optional>
 #include <string>
 #include <string_view>
 #include <vector>
diff --git a/e2e_tests/BUILD b/e2e_tests/BUILD
index fc0e23f..e58abc9 100644
--- a/e2e_tests/BUILD
+++ b/e2e_tests/BUILD
@@ -155,6 +155,7 @@
         "@abseil-cpp//absl/time",
         "@com_google_fuzztest//centipede:weak_sancov_stubs",
         "@com_google_fuzztest//common:logging",
+        "@com_google_fuzztest//common:remote_file",
         "@com_google_fuzztest//common:temp_dir",
         "@com_google_fuzztest//fuzztest/internal:escaping",
         "@com_google_fuzztest//fuzztest/internal:io",
diff --git a/e2e_tests/corpus_database_test.cc b/e2e_tests/corpus_database_test.cc
index 4ce64b9..3f80a9c 100644
--- a/e2e_tests/corpus_database_test.cc
+++ b/e2e_tests/corpus_database_test.cc
@@ -27,8 +27,10 @@
 #include "absl/strings/str_cat.h"
 #include "absl/strings/str_join.h"
 #include "absl/strings/string_view.h"
+#include "absl/time/clock.h"
 #include "absl/time/time.h"
 #include "./common/logging.h"
+#include "./common/remote_file.h"
 #include "./common/temp_dir.h"
 #include "./e2e_tests/test_binary_util.h"
 #include "./fuzztest/internal/escaping.h"
@@ -394,6 +396,101 @@
                                   "test FuzzTest.FailsWithStackOverflow")));
 }
 
+TEST_P(UpdateCorpusDatabaseTest, ReplayDoesNotUpdateDatabaseByDefault) {
+  const std::string db_path = UpdateCorpusDatabaseAndGetPath();
+
+  std::vector<std::string> crash_files;
+  for (const std::string& path : ListDirectoryRecursively(db_path)) {
+    std::filesystem::path fs_path(path);
+    if (fs_path.parent_path().filename() == "crashing" &&
+        fs_path.parent_path().parent_path().filename() ==
+            "FuzzTest.FailsInTwoWays") {
+      crash_files.push_back(path);
+    }
+  }
+  ASSERT_FALSE(crash_files.empty());
+  const std::string target_crash = crash_files[0];
+
+  const auto initial_mtime = RemoteFileGetMTime(target_crash);
+  ASSERT_TRUE(initial_mtime.ok());
+  const absl::Time initial_mod_time = *initial_mtime;
+
+  absl::SleepFor(absl::Seconds(2));
+
+  RunOptions run_options;
+  run_options.fuzztest_flags = {
+      {"corpus_database", db_path},
+      {"replay_corpus", "FuzzTest.FailsInTwoWays"},
+      {"replay_corpus_for", "10s"},
+  };
+
+  auto [status, std_out, std_err] = RunBinaryMaybeWithCentipede(
+      GetCorpusDatabaseTestingBinaryPath(), run_options);
+
+  const auto final_mtime = RemoteFileGetMTime(target_crash);
+  ASSERT_TRUE(final_mtime.ok());
+  EXPECT_EQ(*final_mtime, initial_mod_time);
+}
+
+TEST_P(UpdateCorpusDatabaseTest, ReplayUpdatesDatabaseWithFlag) {
+  const std::string db_path = UpdateCorpusDatabaseAndGetPath();
+
+  std::vector<std::string> crash_files;
+  for (const std::string& path : ListDirectoryRecursively(db_path)) {
+    std::filesystem::path fs_path(path);
+    if (fs_path.parent_path().filename() == "crashing" &&
+        fs_path.parent_path().parent_path().filename() ==
+            "FuzzTest.FailsInTwoWays") {
+      crash_files.push_back(path);
+    }
+  }
+  ASSERT_FALSE(crash_files.empty());
+  const std::string target_crash = crash_files[0];
+
+  const auto initial_mtime = RemoteFileGetMTime(target_crash);
+  ASSERT_TRUE(initial_mtime.ok());
+  const absl::Time initial_mod_time = *initial_mtime;
+
+  absl::SleepFor(absl::Seconds(2));
+
+  RunOptions run_options;
+  run_options.fuzztest_flags = {
+      {"corpus_database", db_path},
+      {"replay_corpus", "FuzzTest.FailsInTwoWays"},
+      {"replay_corpus_for", "10s"},
+      {"update_corpus_database", "true"},
+  };
+
+  auto [status, std_out, std_err] = RunBinaryMaybeWithCentipede(
+      GetCorpusDatabaseTestingBinaryPath(), run_options);
+
+  const auto final_mtime = RemoteFileGetMTime(target_crash);
+  ASSERT_TRUE(final_mtime.ok());
+  EXPECT_GT(*final_mtime, initial_mod_time)
+      << "target_crash: " << target_crash << "\nstd_err:\n"
+      << std_err << "\nstd_out:\n"
+      << std_out;
+}
+
+TEST_P(UpdateCorpusDatabaseTest, FuzzingDoesNotUpdateDatabaseWithFlag) {
+  TempDir corpus_database;
+
+  RunOptions run_options;
+  run_options.fuzztest_flags = {
+      {"corpus_database", corpus_database.path()},
+      {"fuzz_for", "10s"},
+      {"update_corpus_database", "false"},
+  };
+  run_options.timeout = absl::Seconds(10);
+  auto [status, std_out, std_err] = RunBinaryMaybeWithCentipede(
+      GetCorpusDatabaseTestingBinaryPath(), run_options);
+
+  // The database should not be updated, so fuzzing_time should not exist.
+  const absl::StatusOr<std::string> fuzzing_time_file =
+      FindFile(corpus_database.path().c_str(), "fuzzing_time");
+  EXPECT_FALSE(fuzzing_time_file.ok()) << *fuzzing_time_file;
+}
+
 INSTANTIATE_TEST_SUITE_P(
     UpdateCorpusDatabaseTestWithExecutionModel, UpdateCorpusDatabaseTest,
     testing::ValuesIn({
diff --git a/fuzztest/init_fuzztest.cc b/fuzztest/init_fuzztest.cc
index c2fdc44..368cd01 100644
--- a/fuzztest/init_fuzztest.cc
+++ b/fuzztest/init_fuzztest.cc
@@ -144,6 +144,13 @@
     "time budget.");
 
 FUZZTEST_DEFINE_FLAG(
+    std::optional<bool>, update_corpus_database, std::nullopt,
+    "If true, update the corpus database (both crashes and coverage). "
+    "If false, do not update it. "
+    "If unspecified, it defaults to true in fuzzing mode and false in replay "
+    "mode.");
+
+FUZZTEST_DEFINE_FLAG(
     std::optional<std::string>, execution_id, std::nullopt,
     "If set, will resume or skip running on the corpus database for tests that "
     "are previously run with the same execution ID.");
@@ -328,6 +335,13 @@
   return replay_corpus_time_limit;
 }
 
+bool GetDefaultUpdateCorpusDatabase() {
+  const std::string test_to_fuzz = absl::GetFlag(FUZZTEST_FLAG(fuzz));
+  const bool is_fuzzing =
+      test_to_fuzz != kUnspecified || GetFuzzingTime().has_value();
+  return is_fuzzing;
+}
+
 internal::Configuration CreateConfigurationsFromFlags(
     absl::string_view binary_identifier) {
   const bool reproduce_findings_as_separate_tests =
@@ -339,6 +353,10 @@
       absl::GetFlag(FUZZTEST_FLAG(internal_override_fuzz_test));
   const bool replay_coverage_inputs =
       fuzzing_time_limit.has_value() || replay_corpus_time_limit.has_value();
+  const bool update_corpus_database =
+      absl::GetFlag(FUZZTEST_FLAG(update_corpus_database))
+          .value_or(GetDefaultUpdateCorpusDatabase());
+
   const absl::Duration time_limit =
       override_fuzz_test.has_value()
           ? absl::GetFlag(FUZZTEST_FLAG(internal_override_total_time_limit))
@@ -385,6 +403,7 @@
       replay_coverage_inputs,
       /*only_replay=*/
       replay_corpus_time_limit.has_value(),
+      update_corpus_database,
       /*replay_in_single_process=*/false,
       absl::GetFlag(FUZZTEST_FLAG(execution_id)),
       absl::GetFlag(FUZZTEST_FLAG(print_subprocess_log)),
diff --git a/fuzztest/internal/centipede_adaptor.cc b/fuzztest/internal/centipede_adaptor.cc
index 0072126..f13801b 100644
--- a/fuzztest/internal/centipede_adaptor.cc
+++ b/fuzztest/internal/centipede_adaptor.cc
@@ -967,7 +967,8 @@
     runtime_.SetShouldTerminateOnNonFatalFailure(false);
     std::unique_ptr<TempDir> workdir;
     if (configuration.corpus_database.empty() ||
-        (mode == RunMode::kUnitTest && configuration.workdir_root.empty())) {
+        (!configuration.update_corpus_database &&
+         configuration.workdir_root.empty())) {
       workdir = std::make_unique<TempDir>("fuzztest_workdir");
     }
     const std::string workdir_path = workdir ? workdir->path() : "";
diff --git a/fuzztest/internal/configuration.cc b/fuzztest/internal/configuration.cc
index 0d55738..f3d67e2 100644
--- a/fuzztest/internal/configuration.cc
+++ b/fuzztest/internal/configuration.cc
@@ -210,6 +210,7 @@
              SpaceFor(continue_after_crash) +
              SpaceFor(reproduce_findings_as_separate_tests) +
              SpaceFor(replay_coverage_inputs) + SpaceFor(only_replay) +
+             SpaceFor(update_corpus_database) +
              SpaceFor(replay_in_single_process) + SpaceFor(execution_id) +
              SpaceFor(print_subprocess_log) +
              SpaceFor(subprocess_cleanup_timeout_str) + SpaceFor(stack_limit) +
@@ -229,6 +230,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, update_corpus_database);
   offset = WriteIntegral(out, offset, replay_in_single_process);
   offset = WriteOptionalString(out, offset, execution_id);
   offset = WriteIntegral(out, offset, print_subprocess_log);
@@ -261,6 +263,7 @@
                      Consume<bool>(serialized));
     ASSIGN_OR_RETURN(replay_coverage_inputs, Consume<bool>(serialized));
     ASSIGN_OR_RETURN(only_replay, Consume<bool>(serialized));
+    ASSIGN_OR_RETURN(update_corpus_database, 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));
@@ -297,6 +300,7 @@
                          *reproduce_findings_as_separate_tests,
                          *replay_coverage_inputs,
                          *only_replay,
+                         *update_corpus_database,
                          *replay_in_single_process,
                          *std::move(execution_id),
                          *print_subprocess_log,
diff --git a/fuzztest/internal/configuration.h b/fuzztest/internal/configuration.h
index 34e61f6..fd11037 100644
--- a/fuzztest/internal/configuration.h
+++ b/fuzztest/internal/configuration.h
@@ -76,6 +76,8 @@
   bool replay_coverage_inputs = false;
   // If set, further steps are skipped after replaying.
   bool only_replay = false;
+  // If set, update the corpus database (both crashes and coverage).
+  bool update_corpus_database = 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
diff --git a/fuzztest/internal/configuration_test.cc b/fuzztest/internal/configuration_test.cc
index fe79800..d4549bf 100644
--- a/fuzztest/internal/configuration_test.cc
+++ b/fuzztest/internal/configuration_test.cc
@@ -25,6 +25,7 @@
              other->reproduce_findings_as_separate_tests &&
          config.replay_coverage_inputs == other->replay_coverage_inputs &&
          config.only_replay == other->only_replay &&
+         config.update_corpus_database == other->update_corpus_database &&
          config.replay_in_single_process == other->replay_in_single_process &&
          config.execution_id == other->execution_id &&
          config.print_subprocess_log == other->print_subprocess_log &&
@@ -55,6 +56,7 @@
                               /*reproduce_findings_as_separate_tests=*/true,
                               /*replay_coverage_inputs=*/true,
                               /*only_replay=*/true,
+                              /*update_corpus_database=*/true,
                               /*replay_in_single_process=*/true,
                               "execution_id",
                               /*print_subprocess_log=*/true,
@@ -85,6 +87,7 @@
                               /*reproduce_findings_as_separate_tests=*/true,
                               /*replay_coverage_inputs=*/true,
                               /*only_replay=*/true,
+                              /*update_corpus_database=*/true,
                               /*replay_in_single_process=*/true,
                               "execution_id",
                               /*print_subprocess_log=*/true,