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 = [] {