diff --git a/centipede/centipede_flags.inc b/centipede/centipede_flags.inc index 87a8e605b..5d91f66c4 100644 --- a/centipede/centipede_flags.inc +++ b/centipede/centipede_flags.inc @@ -508,6 +508,12 @@ CENTIPEDE_FLAG(std::string, fuzztest_workdir_root, "", CENTIPEDE_FLAG(bool, fuzztest_only_replay, false, "If set, further steps are skipped after replaying with the " "corpus database.") +CENTIPEDE_FLAG( + std::optional, 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 " diff --git a/centipede/centipede_interface.cc b/centipede/centipede_interface.cc index 489bd97e7..6641093f6 100644 --- a/centipede/centipede_interface.cc +++ b/centipede/centipede_interface.cc @@ -335,6 +335,90 @@ PeriodicAction RecordFuzzingTime(std::string_view fuzzing_time_file, 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 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 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 @@ void UpdateCorpusDatabase(Environment env, } }; - 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 @@ void UpdateCorpusDatabase(Environment env, } 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 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 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 @@ int ListCrashIds(const Environment& env) { FUZZTEST_CHECK(!env.test_name.empty()); std::vector 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 37d218943..e45478b4e 100644 --- a/centipede/environment.cc +++ b/centipede/environment.cc @@ -246,6 +246,7 @@ void Environment::UpdateWithTargetConfig( 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 4871fc836..b30956d88 100644 --- a/centipede/environment.h +++ b/centipede/environment.h @@ -19,6 +19,7 @@ #include #include #include +#include #include #include #include diff --git a/e2e_tests/BUILD b/e2e_tests/BUILD index fc0e23f5b..e58abc91a 100644 --- a/e2e_tests/BUILD +++ b/e2e_tests/BUILD @@ -155,6 +155,7 @@ cc_test( "@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 4ce64b915..3f80a9c12 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_P(UpdateCorpusDatabaseTest, PrintsErrorsWhenBazelTimeoutIsNotEnough) { "test FuzzTest.FailsWithStackOverflow"))); } +TEST_P(UpdateCorpusDatabaseTest, ReplayDoesNotUpdateDatabaseByDefault) { + const std::string db_path = UpdateCorpusDatabaseAndGetPath(); + + std::vector 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 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 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 c2fdc44a1..368cd01ac 100644 --- a/fuzztest/init_fuzztest.cc +++ b/fuzztest/init_fuzztest.cc @@ -143,6 +143,13 @@ FUZZTEST_DEFINE_FLAG( ". In the latter case, each FUZZ_TEST will run for at most (1/N)th of the " "time budget."); +FUZZTEST_DEFINE_FLAG( + std::optional, 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, execution_id, std::nullopt, "If set, will resume or skip running on the corpus database for tests that " @@ -328,6 +335,13 @@ std::optional GetReplayCorpusTime() { 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 @@ internal::Configuration CreateConfigurationsFromFlags( 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 @@ internal::Configuration CreateConfigurationsFromFlags( 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 0072126c4..f13801bf8 100644 --- a/fuzztest/internal/centipede_adaptor.cc +++ b/fuzztest/internal/centipede_adaptor.cc @@ -967,7 +967,8 @@ bool CentipedeFuzzerAdaptor::Run(int* argc, char*** argv, RunMode mode, runtime_.SetShouldTerminateOnNonFatalFailure(false); std::unique_ptr 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("fuzztest_workdir"); } const std::string workdir_path = workdir ? workdir->path() : ""; diff --git a/fuzztest/internal/configuration.cc b/fuzztest/internal/configuration.cc index 0d5573890..f3d67e249 100644 --- a/fuzztest/internal/configuration.cc +++ b/fuzztest/internal/configuration.cc @@ -210,6 +210,7 @@ std::string Configuration::Serialize() const { 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 @@ std::string Configuration::Serialize() const { 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 @@ absl::StatusOr Configuration::Deserialize( Consume(serialized)); ASSIGN_OR_RETURN(replay_coverage_inputs, Consume(serialized)); ASSIGN_OR_RETURN(only_replay, Consume(serialized)); + ASSIGN_OR_RETURN(update_corpus_database, Consume(serialized)); ASSIGN_OR_RETURN(replay_in_single_process, Consume(serialized)); ASSIGN_OR_RETURN(execution_id, ConsumeOptionalString(serialized)); ASSIGN_OR_RETURN(print_subprocess_log, Consume(serialized)); @@ -297,6 +300,7 @@ absl::StatusOr Configuration::Deserialize( *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 34e61f6a9..fd110378b 100644 --- a/fuzztest/internal/configuration.h +++ b/fuzztest/internal/configuration.h @@ -76,6 +76,8 @@ struct Configuration { 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 fe79800e3..d4549bf6d 100644 --- a/fuzztest/internal/configuration_test.cc +++ b/fuzztest/internal/configuration_test.cc @@ -25,6 +25,7 @@ MATCHER_P(IsOkAndEquals, config, "") { 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 @@ TEST(ConfigurationTest, /*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 @@ TEST(ConfigurationTest, /*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,