diff --git a/centipede/centipede_interface.cc b/centipede/centipede_interface.cc index e6ba1ebcf..1e26b9509 100644 --- a/centipede/centipede_interface.cc +++ b/centipede/centipede_interface.cc @@ -75,6 +75,7 @@ namespace fuzztest::internal { namespace { constexpr absl::Duration kDefaultRegressionTtl = absl::Hours(24 * 7); +constexpr double kCrashDeduplicationTimeFraction = 0.25; // Runs env.for_each_blob on every blob extracted from env.args. // Returns EXIT_SUCCESS on success, EXIT_FAILURE otherwise. @@ -419,6 +420,14 @@ void RecordFuzzingResults(const Environment& env, const DatabasePaths& db_paths, } } +absl::Duration GetCrashDeduplicationTimeLimit( + absl::Duration fuzzing_time_limit) { + if (fuzzing_time_limit == absl::InfiniteDuration()) { + return absl::InfiniteDuration(); + } + return kCrashDeduplicationTimeFraction * fuzzing_time_limit; +} + void UpdateCorpusDatabase(Environment env, CentipedeCallbacksFactory& callbacks_factory, StopCondition& stop_condition) { @@ -610,8 +619,11 @@ void UpdateCorpusDatabase(Environment env, return; } - // The test time limit does not apply for updating the corpus database. - stop_condition.SetStopTime(absl::InfiniteFuture()); + const absl::Duration dedup_time_limit = + GetCrashDeduplicationTimeLimit(time_limit); + FUZZTEST_LOG(INFO) << "Dedicating " << dedup_time_limit + << " to updating corpus database for " << env.test_name; + stop_condition.SetStopTime(absl::Now() + dedup_time_limit); RecordFuzzingResults(env, db_paths, callbacks_factory, stop_condition); } diff --git a/centipede/crash_deduplication.cc b/centipede/crash_deduplication.cc index 397110564..0f5f4a708 100644 --- a/centipede/crash_deduplication.cc +++ b/centipede/crash_deduplication.cc @@ -152,58 +152,72 @@ absl::StatusOr> ReadIncubatingCrashes( return incubating_crashes; } -absl::Status ReplayCrash(CentipedeCallbacks& callbacks, const Environment& env, - absl::string_view input_path, - std::string& out_signature, - std::string& out_description) { - ByteArray input_bytes; - RETURN_IF_NOT_OK(RemoteFileGetContents(input_path, input_bytes)); +struct CrashToReplay { + CrashDetails& details; + std::string& new_signature; + ByteArray input_bytes = {}; +}; - const size_t max_attempts = std::max(1, env.replay_crash_attempts); - for (size_t attempt = 0; attempt < max_attempts; ++attempt) { - BatchResult batch_result; - if (!callbacks.Execute(env.binary, {input_bytes}, batch_result) && - batch_result.IsInputFailure()) { - out_signature = batch_result.failure_signature(); - out_description = batch_result.failure_description(); - if (attempt > 0) { - FUZZTEST_LOG(INFO) << "Crash reproduced on attempt " << (attempt + 1) - << " of " << max_attempts << " for " << input_path; - } - return absl::OkStatus(); - } - } +std::optional ToCrashToReplay(ExistingCrash& existing) { + if (existing.crash_report.signature.empty()) return std::nullopt; + return CrashToReplay{existing.crash_report.details, existing.new_signature}; +} - if (max_attempts > 1) { - FUZZTEST_LOG(INFO) << "Crash failed to reproduce after " << max_attempts - << " attempts for " << input_path; - } - out_signature = ""; - out_description = ""; - return absl::OkStatus(); +std::optional ToCrashToReplay(IncubatingCrash& incubating) { + return CrashToReplay{incubating.details, incubating.new_signature}; } -absl::Status ReplayExistingCrashes( - CentipedeCallbacks& callbacks, const Environment& env, - std::vector& existing_crashes) { - for (auto& existing : existing_crashes) { - if (existing.crash_report.signature.empty()) { - continue; - } - RETURN_IF_NOT_OK(ReplayCrash( - callbacks, env, existing.crash_report.details.input_path, - existing.new_signature, existing.crash_report.details.description)); +template +absl::StatusOr> ToCrashesToReplay( + absl::Span raw_crashes) { + std::vector crashes; + crashes.reserve(raw_crashes.size()); + for (CrashT& raw_crash : raw_crashes) { + std::optional crash = ToCrashToReplay(raw_crash); + if (!crash.has_value()) continue; + RETURN_IF_NOT_OK( + RemoteFileGetContents(crash->details.input_path, crash->input_bytes)); + crashes.push_back(*std::move(crash)); } - return absl::OkStatus(); + return crashes; } -absl::Status ReplayIncubatingCrashes( - CentipedeCallbacks& callbacks, const Environment& env, - std::vector& incubating_crashes) { - for (auto& incubating : incubating_crashes) { - RETURN_IF_NOT_OK(ReplayCrash(callbacks, env, incubating.details.input_path, - incubating.new_signature, - incubating.details.description)); +absl::Status ReplayCrashes(CentipedeCallbacks& callbacks, + const Environment& env, + const StopCondition& stop_condition, + absl::Span crashes) { + for (CrashToReplay& crash : crashes) { + crash.new_signature.clear(); + crash.details.description.clear(); + } + + const size_t max_attempts = std::max(1, env.replay_crash_attempts); + for (size_t attempt = 0; attempt < max_attempts; ++attempt) { + if (stop_condition.ShouldStop()) break; + bool any_remaining = false; + for (CrashToReplay& crash : crashes) { + if (stop_condition.ShouldStop()) break; + if (!crash.new_signature.empty()) continue; + BatchResult batch_result; + if (!callbacks.Execute(env.binary, {crash.input_bytes}, batch_result) && + batch_result.IsInputFailure()) { + crash.new_signature = batch_result.failure_signature(); + crash.details.description = batch_result.failure_description(); + if (attempt > 0) { + FUZZTEST_LOG(INFO) + << "Crash reproduced on attempt " << (attempt + 1) << " of " + << max_attempts << " for " << crash.details.input_path; + } + } else { + any_remaining = true; + if (attempt + 1 == max_attempts && max_attempts > 1) { + FUZZTEST_LOG(INFO) + << "Crash failed to reproduce after " << max_attempts + << " attempts for " << crash.details.input_path; + } + } + } + if (!any_remaining) break; } return absl::OkStatus(); } @@ -620,10 +634,18 @@ absl::Status OrganizeCrashingInputs( ScopedCentipedeCallbacks scoped_callbacks(callbacks_factory, env, stop_condition); - RETURN_IF_NOT_OK(ReplayExistingCrashes(*scoped_callbacks.callbacks(), env, - existing_crashes)); - RETURN_IF_NOT_OK(ReplayIncubatingCrashes(*scoped_callbacks.callbacks(), env, - incubating_crashes)); + ASSIGN_OR_RETURN_IF_NOT_OK( + std::vector existing_to_replay, + ToCrashesToReplay(absl::MakeSpan(existing_crashes))); + RETURN_IF_NOT_OK(ReplayCrashes(*scoped_callbacks.callbacks(), env, + stop_condition, + absl::MakeSpan(existing_to_replay))); + ASSIGN_OR_RETURN_IF_NOT_OK( + std::vector incubating_to_replay, + ToCrashesToReplay(absl::MakeSpan(incubating_crashes))); + RETURN_IF_NOT_OK(ReplayCrashes(*scoped_callbacks.callbacks(), env, + stop_condition, + absl::MakeSpan(incubating_to_replay))); absl::flat_hash_map new_crashes = FindNewCrashes( new_crashes_by_signature, incubating_crashes, existing_crashes); diff --git a/centipede/crash_deduplication_test.cc b/centipede/crash_deduplication_test.cc index b65f5579d..9538aa5ff 100644 --- a/centipede/crash_deduplication_test.cc +++ b/centipede/crash_deduplication_test.cc @@ -228,6 +228,7 @@ class OrganizeCrashingInputsTest : public ::testing::Test { } const Environment& env() const { return env_; } CrashSummary& crash_summary() { return crash_summary_; } + StopCondition& stop_condition() { return stop_condition_; } absl::Status OrganizeCrashingInputs( const std::filesystem::path& regression_dir, @@ -1190,5 +1191,101 @@ TEST_F(OrganizeCrashingInputsTest, ReplaysCrashUpToMaxAttemptsOnFailure) { HasSubstr("Crash failed to reproduce after 3 attempts for ")); } +class RecordingCrashCallbacks : public CentipedeCallbacks { + public: + RecordingCrashCallbacks( + const Environment& env, StopCondition& stop_condition, + absl::flat_hash_map crash_on_input_attempt, + int stop_after_total_executions = -1) + : CentipedeCallbacks(env, stop_condition), + crash_on_input_attempt_(std::move(crash_on_input_attempt)), + stop_after_total_executions_(stop_after_total_executions) {} + + bool Execute(std::string_view binary, absl::Span inputs, + BatchResult& batch_result) override { + batch_result.ClearAndResize(inputs.size()); + std::string input_str(AsStringView(inputs[0])); + executed_inputs_.push_back(input_str); + const int attempt = ++attempts_by_input_[input_str]; + if (stop_after_total_executions_ > 0 && + static_cast(executed_inputs_.size()) >= + stop_after_total_executions_) { + stop_condition_.SetStopTime(absl::InfinitePast()); + } + auto it = crash_on_input_attempt_.find(input_str); + if (it != crash_on_input_attempt_.end() && attempt == it->second) { + batch_result.exit_code() = EXIT_FAILURE; + batch_result.failure_signature() = "csig_" + input_str; + batch_result.failure_description() = "desc_" + input_str; + return false; + } + return true; + } + + const std::vector& executed_inputs() const { + return executed_inputs_; + } + + private: + absl::flat_hash_map crash_on_input_attempt_; + int stop_after_total_executions_; + absl::flat_hash_map attempts_by_input_; + std::vector executed_inputs_; +}; + +TEST_F(OrganizeCrashingInputsTest, ReplaysMultipleCrashesInRoundRobinOrder) { + SetContentsAndGetPath(crashing_dir(), "bug1-csig_input1-isig1", "input1"); + SetContentsAndGetPath(crashing_dir(), "bug2-csig_input2-isig2", "input2"); + + Environment test_env = env(); + test_env.replay_crash_attempts = 3; + + // input2 reproduces on its 2nd attempt; input1 never reproduces. + RecordingCrashCallbacks callbacks(test_env, stop_condition(), + /*crash_on_input_attempt=*/{{"input2", 2}}); + NonOwningCallbacksFactory factory(callbacks); + + ASSERT_TRUE(OrganizeCrashingInputs(regression_dir(), crashing_dir(), test_env, + factory, /*new_crashes_by_signature=*/{}, + crash_summary()) + .ok()); + + const auto& executed = callbacks.executed_inputs(); + ASSERT_EQ(executed.size(), 5); + EXPECT_THAT(absl::MakeConstSpan(executed).subspan(0, 2), + UnorderedElementsAre("input1", "input2")); + EXPECT_THAT(absl::MakeConstSpan(executed).subspan(2, 2), + UnorderedElementsAre("input1", "input2")); + EXPECT_EQ(executed[4], "input1"); +} + +TEST_F(OrganizeCrashingInputsTest, StopsReplayingWhenStopConditionIsTriggered) { + SetContentsAndGetPath(crashing_dir(), "bug1-csig_input1-isig1", "input1"); + SetContentsAndGetPath(crashing_dir(), "bug2-csig_input2-isig2", "input2"); + + Environment test_env = env(); + test_env.replay_crash_attempts = 5; + + // Trigger stop_condition after 2 total executions (1 pass over the 2 inputs). + RecordingCrashCallbacks callbacks(test_env, stop_condition(), + /*crash_on_input_attempt=*/{{"input2", 1}}, + /*stop_after_total_executions=*/2); + NonOwningCallbacksFactory factory(callbacks); + + ASSERT_TRUE(OrganizeCrashingInputs(regression_dir(), crashing_dir(), test_env, + factory, /*new_crashes_by_signature=*/{}, + crash_summary()) + .ok()); + + // Only the first pass (2 executions) should run before stop_condition halts + // further retries, and input2 (which reproduced on pass 1) is still reported. + EXPECT_EQ(callbacks.executed_inputs().size(), 2); + std::string crash_report; + crash_summary().Report(&crash_report); + EXPECT_THAT(crash_report, + AllOf(HasSubstr("Total crashes: 1"), + HasSubstr("Crash ID : bug2-csig_input2-isig2"))); +} + } // namespace } // namespace fuzztest::internal