Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions centipede/centipede_interface.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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);
}

Expand Down
120 changes: 71 additions & 49 deletions centipede/crash_deduplication.cc
Original file line number Diff line number Diff line change
Expand Up @@ -152,58 +152,72 @@ absl::StatusOr<std::vector<IncubatingCrash>> 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<size_t>(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<CrashToReplay> 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<CrashToReplay> ToCrashToReplay(IncubatingCrash& incubating) {
return CrashToReplay{incubating.details, incubating.new_signature};
}

absl::Status ReplayExistingCrashes(
CentipedeCallbacks& callbacks, const Environment& env,
std::vector<ExistingCrash>& 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 <typename CrashT>
absl::StatusOr<std::vector<CrashToReplay>> ToCrashesToReplay(
absl::Span<CrashT> raw_crashes) {
std::vector<CrashToReplay> crashes;
crashes.reserve(raw_crashes.size());
for (CrashT& raw_crash : raw_crashes) {
std::optional<CrashToReplay> 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<IncubatingCrash>& 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<CrashToReplay> crashes) {
for (CrashToReplay& crash : crashes) {
crash.new_signature.clear();
crash.details.description.clear();
}

const size_t max_attempts = std::max<size_t>(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();
}
Expand Down Expand Up @@ -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<CrashToReplay> 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<CrashToReplay> 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<std::string, CrashDetails> new_crashes = FindNewCrashes(
new_crashes_by_signature, incubating_crashes, existing_crashes);
Expand Down
97 changes: 97 additions & 0 deletions centipede/crash_deduplication_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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<std::string, int> 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<const ByteSpan> 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<int>(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<std::string>& executed_inputs() const {
return executed_inputs_;
}

private:
absl::flat_hash_map<std::string, int> crash_on_input_attempt_;
int stop_after_total_executions_;
absl::flat_hash_map<std::string, int> attempts_by_input_;
std::vector<std::string> 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
Loading