Skip to content

Commit 13130a3

Browse files
xinhaoyuancopybara-github
authored andcommitted
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
1 parent 59dff64 commit 13130a3

9 files changed

+168
-20
lines changed

centipede/centipede_interface.cc

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -694,6 +694,70 @@ int UpdateCorpusDatabaseForFuzzTests(
694694
return EXIT_SUCCESS;
695695
}
696696

697+
int ReplayCrash(const Environment &env,
698+
const fuzztest::internal::Configuration &target_config,
699+
CentipedeCallbacksFactory &callbacks_factory) {
700+
CHECK(!env.crash_id.empty()) << "Need crash_id to be set for replay a crash";
701+
CHECK(target_config.fuzz_tests_in_current_shard.size() == 1)
702+
<< "Expecting exactly one test for replay_crash";
703+
// TODO: b/406003594 - move the path construction to a libarary.
704+
const auto crash_dir = std::filesystem::path(target_config.corpus_database) /
705+
target_config.binary_identifier /
706+
target_config.fuzz_tests_in_current_shard[0] /
707+
"crashing";
708+
const WorkDir workdir{env};
709+
SeedCorpusSource crash_corpus_source;
710+
crash_corpus_source.dir_glob = crash_dir;
711+
crash_corpus_source.num_recent_dirs = 1;
712+
crash_corpus_source.individual_input_rel_glob = env.crash_id;
713+
crash_corpus_source.sampled_fraction_or_count = 1.0f;
714+
const SeedCorpusConfig crash_corpus_config = {
715+
/*sources=*/{crash_corpus_source},
716+
/*destination=*/{
717+
/*dir_path=*/env.workdir,
718+
/*shard_rel_glob=*/
719+
std::filesystem::path{workdir.CorpusFilePaths().AllShardsGlob()}
720+
.filename(),
721+
/*shard_index_digits=*/WorkDir::kDigitsInShardIndex,
722+
/*num_shards=*/1}};
723+
CHECK_OK(GenerateSeedCorpusFromConfig(crash_corpus_config, env.binary_name,
724+
env.binary_hash));
725+
Environment run_crash_env = env;
726+
run_crash_env.load_shards_only = true;
727+
return Fuzz(run_crash_env, {}, "", callbacks_factory);
728+
}
729+
730+
int ExportCrash(const Environment &env,
731+
const fuzztest::internal::Configuration &target_config) {
732+
CHECK(!env.crash_id.empty())
733+
<< "Need crash_id to be set for exporting a crash";
734+
CHECK(!env.export_crash_file.empty())
735+
<< "Need export_crash_file to be set for exporting a crash";
736+
CHECK(target_config.fuzz_tests_in_current_shard.size() == 1)
737+
<< "Expecting exactly one test for exporting a crash";
738+
// TODO: b/406003594 - move the path construction to a libarary.
739+
const auto crash_dir = std::filesystem::path(target_config.corpus_database) /
740+
target_config.binary_identifier /
741+
target_config.fuzz_tests_in_current_shard[0] /
742+
"crashing";
743+
std::string crash_contents;
744+
const auto read_status =
745+
RemoteFileGetContents((crash_dir / env.crash_id).c_str(), crash_contents);
746+
if (!read_status.ok()) {
747+
LOG(ERROR) << "Failed reading the crash " << env.crash_id << " from "
748+
<< crash_dir.c_str() << ": " << read_status;
749+
return EXIT_FAILURE;
750+
}
751+
const auto write_status =
752+
RemoteFileSetContents(env.export_crash_file, crash_contents);
753+
if (!write_status.ok()) {
754+
LOG(ERROR) << "Failed write the crash " << env.crash_id << " to "
755+
<< env.export_crash_file << ": " << write_status;
756+
return EXIT_FAILURE;
757+
}
758+
return EXIT_SUCCESS;
759+
}
760+
697761
} // namespace
698762

699763
int CentipedeMain(const Environment &env,
@@ -755,6 +819,15 @@ int CentipedeMain(const Environment &env,
755819
CHECK_OK(target_config.status())
756820
<< "Failed to deserialize target configuration";
757821
if (!target_config->corpus_database.empty()) {
822+
CHECK(!env.replay_crash || !env.export_crash)
823+
<< "replay_crash and export_crash cannot be both set";
824+
if (env.replay_crash) {
825+
return ReplayCrash(env, *target_config, callbacks_factory);
826+
}
827+
if (env.export_crash) {
828+
return ExportCrash(env, *target_config);
829+
}
830+
758831
const auto time_limit_per_test = target_config->GetTimeLimitPerTest();
759832
CHECK(target_config->only_replay ||
760833
time_limit_per_test < absl::InfiniteDuration())

centipede/environment.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,14 @@ struct Environment {
135135
// If set, deserializes the configuration from the value instead of querying
136136
// the configuration via runner callbacks.
137137
std::string fuzztest_configuration;
138+
// The crash ID used for `replay_crash` or `export_crash`.
139+
std::string crash_id;
140+
// If set, replay `crash_id` in the corpus database.
141+
bool replay_crash = false;
142+
// If set, export the input contents of `crash_id` from the corpus database.
143+
bool export_crash = false;
144+
// The path to export the input contents of `crash_id` for `export_crash`.
145+
std::string export_crash_file;
138146

139147
// Command line-related fields -----------------------------------------------
140148

centipede/environment_flags.cc

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -536,6 +536,10 @@ Environment CreateEnvironmentFromFlags(const std::vector<std::string> &argv) {
536536
/*fuzztest_single_test_mode=*/
537537
Environment::Default().fuzztest_single_test_mode,
538538
/*fuzztest_configuration=*/Environment::Default().fuzztest_configuration,
539+
/*crash_id=*/Environment::Default().crash_id,
540+
/*replay_crash=*/Environment::Default().replay_crash,
541+
/*export_crash=*/Environment::Default().export_crash,
542+
/*export_crash_file=*/Environment::Default().export_crash_file,
539543
/*exec_name=*/Environment::Default().exec_name,
540544
/*args=*/Environment::Default().args,
541545
/*binary_name=*/

fuzztest/init_fuzztest.cc

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,8 @@ FUZZTEST_DEFINE_FLAG(
176176
//
177177
// These flags are meant to be set only by the parent controller process for its
178178
// child processes.
179+
//
180+
// TODO(b/406001082): Remove these flags once they are no longer needed.
179181

180182
FUZZTEST_DEFINE_FLAG(
181183
std::optional<std::string>, internal_override_fuzz_test, std::nullopt,
@@ -206,6 +208,21 @@ FUZZTEST_DEFINE_FLAG(
206208
"internal_override_total_time_limit directly");
207209
});
208210

211+
FUZZTEST_DEFINE_FLAG(std::optional<std::string>,
212+
internal_crashing_input_to_reproduce, std::nullopt,
213+
"Internal-only flag - do not use directly. If both this "
214+
"and --" FUZZTEST_FLAG_PREFIX
215+
"internal_override_fuzz_test are set, replay "
216+
"the input in the corpus database with the specified ID.")
217+
.OnUpdate([] {
218+
FUZZTEST_INTERNAL_CHECK_PRECONDITION(
219+
!absl::GetFlag(FUZZTEST_FLAG(internal_crashing_input_to_reproduce))
220+
.has_value() ||
221+
std::getenv("CENTIPEDE_RUNNER_FLAGS") != nullptr,
222+
"must not set --" FUZZTEST_FLAG_PREFIX
223+
"internal_crashing_input_to_reproduce directly");
224+
});
225+
209226
namespace fuzztest {
210227

211228
std::vector<std::string> ListRegisteredTests() {
@@ -314,12 +331,14 @@ internal::Configuration CreateConfigurationsFromFlags(
314331
reproduce_findings_as_separate_tests, replay_coverage_inputs,
315332
/*only_replay=*/
316333
replay_corpus_time_limit.has_value(),
334+
/*replay_in_single_process=*/false,
317335
absl::GetFlag(FUZZTEST_FLAG(execution_id)),
318336
absl::GetFlag(FUZZTEST_FLAG(print_subprocess_log)),
319337
/*stack_limit=*/absl::GetFlag(FUZZTEST_FLAG(stack_limit_kb)) * 1024,
320338
/*rss_limit=*/absl::GetFlag(FUZZTEST_FLAG(rss_limit_mb)) * 1024 * 1024,
321339
absl::GetFlag(FUZZTEST_FLAG(time_limit_per_input)), time_limit,
322-
time_budget_type, jobs.value_or(0)};
340+
time_budget_type, jobs.value_or(0),
341+
absl::GetFlag(FUZZTEST_FLAG(internal_crashing_input_to_reproduce))};
323342
}
324343
} // namespace
325344

fuzztest/internal/centipede_adaptor.cc

Lines changed: 42 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,16 @@ centipede::Environment CreateCentipedeEnvironmentFromConfiguration(
239239
" --" FUZZTEST_FLAG_PREFIX
240240
"internal_override_total_time_limit=",
241241
total_time_limit);
242+
if (configuration.crashing_input_to_reproduce.has_value()) {
243+
absl::StrAppend(&env.binary,
244+
" --" FUZZTEST_FLAG_PREFIX
245+
"internal_crashing_input_to_reproduce=",
246+
*configuration.crashing_input_to_reproduce);
247+
env.crash_id =
248+
std::filesystem::path(*configuration.crashing_input_to_reproduce)
249+
.filename();
250+
env.replay_crash = true;
251+
}
242252
env.coverage_binary = (*args)[0];
243253
env.binary_name = std::filesystem::path{(*args)[0]}.filename();
244254
env.binary_hash = GetSelfBinaryHashForCentipedeEnvironment();
@@ -586,7 +596,9 @@ bool CentipedeFuzzerAdaptor::Run(int* argc, char*** argv, RunMode mode,
586596
// and we should not run CentipedeMain in this process.
587597
const bool runner_mode = std::getenv("CENTIPEDE_RUNNER_FLAGS");
588598
const bool is_running_property_function_in_this_process =
589-
runner_mode || configuration.crashing_input_to_reproduce.has_value() ||
599+
runner_mode ||
600+
(configuration.crashing_input_to_reproduce.has_value() &&
601+
configuration.replay_in_single_process) ||
590602
std::getenv("FUZZTEST_REPLAY") ||
591603
std::getenv("FUZZTEST_MINIMIZE_REPRODUCER");
592604
if (!is_running_property_function_in_this_process &&
@@ -627,7 +639,10 @@ bool CentipedeFuzzerAdaptor::Run(int* argc, char*** argv, RunMode mode,
627639
// Centipede engine does not support replay and reproducer minimization
628640
// (within the single process). So use the existing fuzztest implementation.
629641
// This is fine because it does not require coverage instrumentation.
630-
if (fuzzer_impl_.ReplayInputsIfAvailable(configuration)) return 0;
642+
if (!configuration.crashing_input_to_reproduce.has_value() &&
643+
fuzzer_impl_.ReplayInputsIfAvailable(configuration)) {
644+
return 0;
645+
}
631646
// `ReplayInputsIfAvailable` overwrites the run mode - revert it back.
632647
runtime_.SetRunMode(mode);
633648
// Tear down fixture early to avoid interfering with the runners.
@@ -643,10 +658,31 @@ bool CentipedeFuzzerAdaptor::Run(int* argc, char*** argv, RunMode mode,
643658
configuration, workdir_path, test_.full_name(), mode);
644659
centipede::DefaultCallbacksFactory<centipede::CentipedeDefaultCallbacks>
645660
factory;
646-
if (const char* minimize_dir_chars =
647-
std::getenv("FUZZTEST_MINIMIZE_TESTSUITE_DIR");
648-
configuration.corpus_database.empty() &&
649-
minimize_dir_chars != nullptr) {
661+
if (!configuration.corpus_database.empty()) {
662+
if (!env.crash_id.empty() && configuration.replay_in_single_process) {
663+
TempDir crash_fetch_dir("fuzztest_crash");
664+
auto export_crash_env = env;
665+
std::string crash_file =
666+
(std::filesystem::path(crash_fetch_dir.path()) / "crash").string();
667+
export_crash_env.export_crash_file = crash_file;
668+
export_crash_env.replay_crash = false;
669+
export_crash_env.export_crash = true;
670+
if (centipede::CentipedeMain(export_crash_env, factory) !=
671+
EXIT_SUCCESS) {
672+
absl::FPrintF(
673+
GetStderr(),
674+
"[!] Encountered error when using Centipede to export the crash "
675+
"input.");
676+
return EXIT_FAILURE;
677+
}
678+
CentipedeAdaptorRunnerCallbacks runner_callbacks(
679+
&runtime_, &fuzzer_impl_, &configuration);
680+
static char replay_argv0[] = "replay_argv";
681+
char* replay_argv[] = {replay_argv0, crash_file.data()};
682+
return centipede::RunnerMain(/*argc=*/2, replay_argv, runner_callbacks);
683+
}
684+
} else if (const char* minimize_dir_chars =
685+
std::getenv("FUZZTEST_MINIMIZE_TESTSUITE_DIR")) {
650686
const std::string minimize_dir = minimize_dir_chars;
651687
const char* corpus_out_dir_chars =
652688
std::getenv("FUZZTEST_TESTSUITE_OUT_DIR");

fuzztest/internal/configuration.cc

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -207,11 +207,11 @@ std::string Configuration::Serialize() const {
207207
SpaceFor(fuzz_tests) + SpaceFor(fuzz_tests_in_current_shard) +
208208
SpaceFor(reproduce_findings_as_separate_tests) +
209209
SpaceFor(replay_coverage_inputs) + SpaceFor(only_replay) +
210-
SpaceFor(execution_id) + SpaceFor(print_subprocess_log) +
211-
SpaceFor(stack_limit) + SpaceFor(rss_limit) +
212-
SpaceFor(time_limit_per_input_str) + SpaceFor(time_limit_str) +
213-
SpaceFor(time_budget_type_str) + SpaceFor(jobs) +
214-
SpaceFor(crashing_input_to_reproduce) +
210+
SpaceFor(replay_in_single_process) + SpaceFor(execution_id) +
211+
SpaceFor(print_subprocess_log) + SpaceFor(stack_limit) +
212+
SpaceFor(rss_limit) + SpaceFor(time_limit_per_input_str) +
213+
SpaceFor(time_limit_str) + SpaceFor(time_budget_type_str) +
214+
SpaceFor(jobs) + SpaceFor(crashing_input_to_reproduce) +
215215
SpaceFor(reproduction_command_template));
216216
size_t offset = 0;
217217
offset = WriteString(out, offset, corpus_database);
@@ -223,6 +223,7 @@ std::string Configuration::Serialize() const {
223223
offset = WriteIntegral(out, offset, reproduce_findings_as_separate_tests);
224224
offset = WriteIntegral(out, offset, replay_coverage_inputs);
225225
offset = WriteIntegral(out, offset, only_replay);
226+
offset = WriteIntegral(out, offset, replay_in_single_process);
226227
offset = WriteOptionalString(out, offset, execution_id);
227228
offset = WriteIntegral(out, offset, print_subprocess_log);
228229
offset = WriteIntegral(out, offset, stack_limit);
@@ -251,6 +252,7 @@ absl::StatusOr<Configuration> Configuration::Deserialize(
251252
Consume<bool>(serialized));
252253
ASSIGN_OR_RETURN(replay_coverage_inputs, Consume<bool>(serialized));
253254
ASSIGN_OR_RETURN(only_replay, Consume<bool>(serialized));
255+
ASSIGN_OR_RETURN(replay_in_single_process, Consume<bool>(serialized));
254256
ASSIGN_OR_RETURN(execution_id, ConsumeOptionalString(serialized));
255257
ASSIGN_OR_RETURN(print_subprocess_log, Consume<bool>(serialized));
256258
ASSIGN_OR_RETURN(stack_limit, Consume<size_t>(serialized));
@@ -281,6 +283,7 @@ absl::StatusOr<Configuration> Configuration::Deserialize(
281283
*reproduce_findings_as_separate_tests,
282284
*replay_coverage_inputs,
283285
*only_replay,
286+
*replay_in_single_process,
284287
*std::move(execution_id),
285288
*print_subprocess_log,
286289
*stack_limit,

fuzztest/internal/configuration.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,8 @@ struct Configuration {
7474
bool replay_coverage_inputs = false;
7575
// If set, further steps are skipped after replaying.
7676
bool only_replay = false;
77+
// If set, replay without spawning subprocesses.
78+
bool replay_in_single_process = false;
7779
// If set, will be used when working on a corpus database to resume
7880
// the progress in case the execution got interrupted.
7981
std::optional<std::string> execution_id;

fuzztest/internal/configuration_test.cc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ MATCHER_P(IsOkAndEquals, config, "") {
2424
other->reproduce_findings_as_separate_tests &&
2525
config.replay_coverage_inputs == other->replay_coverage_inputs &&
2626
config.only_replay == other->only_replay &&
27+
config.replay_in_single_process == other->replay_in_single_process &&
2728
config.execution_id == other->execution_id &&
2829
config.print_subprocess_log == other->print_subprocess_log &&
2930
config.stack_limit == other->stack_limit &&
@@ -49,6 +50,7 @@ TEST(ConfigurationTest,
4950
/*reproduce_findings_as_separate_tests=*/true,
5051
/*replay_coverage_inputs=*/true,
5152
/*only_replay=*/true,
53+
/*replay_in_single_process=*/true,
5254
"execution_id",
5355
/*print_subprocess_log=*/true,
5456
/*stack_limit=*/100,
@@ -75,6 +77,7 @@ TEST(ConfigurationTest,
7577
/*reproduce_findings_as_separate_tests=*/true,
7678
/*replay_coverage_inputs=*/true,
7779
/*only_replay=*/true,
80+
/*replay_in_single_process=*/true,
7881
"execution_id",
7982
/*print_subprocess_log=*/true,
8083
/*stack_limit=*/100,

fuzztest/internal/googletest_adaptor.h

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -40,20 +40,20 @@ class GTest_TestAdaptor : public ::testing::Test {
4040
void TestBody() override {
4141
auto test = test_.make();
4242
configuration_.fuzz_tests_in_current_shard = GetFuzzTestsInCurrentShard();
43+
configuration_.replay_in_single_process =
44+
configuration_.crashing_input_to_reproduce.has_value() &&
45+
testing::UnitTest::GetInstance()->test_to_run_count() == 1;
4346
if (Runtime::instance().run_mode() == RunMode::kUnitTest) {
4447
// In "bug reproduction" mode, sometimes we need to reproduce multiple
4548
// bugs, i.e., run multiple tests that lead to a crash.
4649
bool needs_subprocess = false;
47-
#ifdef GTEST_HAS_DEATH_TEST
50+
#if defined(GTEST_HAS_DEATH_TEST) && !defined(FUZZTEST_USE_CENTIPEDE)
4851
needs_subprocess =
4952
configuration_.crashing_input_to_reproduce.has_value() &&
50-
(
51-
// When only a single test runs, it's okay to crash the process on
52-
// error, as we don't need to run other tests.
53-
testing::UnitTest::GetInstance()->test_to_run_count() > 1 ||
54-
// EXPECT_EXIT is required in the death-test subprocess, but in
55-
// the subprocess there's only one test to run.
56-
testing::internal::InDeathTestChild());
53+
(!configuration_.replay_in_single_process ||
54+
// EXPECT_EXIT is required in the death-test subprocess, but in
55+
// the subprocess there's only one test to run.
56+
testing::internal::InDeathTestChild());
5757
#endif
5858
if (needs_subprocess) {
5959
configuration_.preprocess_crash_reproducing = [] {

0 commit comments

Comments
 (0)