Report a summary of crashes at the end of an "update corpus database" session. In a follow-up CL I'll add crash summaries at the end of other actions that can expose crashes (replay crash, regular fuzz without updating the corpus database). PiperOrigin-RevId: 777596299
diff --git a/centipede/BUILD b/centipede/BUILD index 6366036..c43809e 100644 --- a/centipede/BUILD +++ b/centipede/BUILD
@@ -815,6 +815,7 @@ ":centipede_lib", ":command", ":coverage", + ":crash_summary", ":distill", ":environment", ":minimize_crash", @@ -952,6 +953,18 @@ ) cc_library( + name = "crash_summary", + srcs = ["crash_summary.cc"], + hdrs = ["crash_summary.h"], + deps = [ + ":util", + "@abseil-cpp//absl/strings:str_format", + "@abseil-cpp//absl/types:span", + "@com_google_fuzztest//common:defs", + ], +) + +cc_library( name = "weak_sancov_stubs", srcs = ["weak_sancov_stubs.cc"], visibility = ["//visibility:public"], @@ -1840,6 +1853,15 @@ ], ) +cc_test( + name = "crash_summary_test", + srcs = ["crash_summary_test.cc"], + deps = [ + ":crash_summary", + "@googletest//:gtest_main", + ], +) + ################################################################################ # Other tests ################################################################################
diff --git a/centipede/centipede_interface.cc b/centipede/centipede_interface.cc index 77b7a2d..fbf90db 100644 --- a/centipede/centipede_interface.cc +++ b/centipede/centipede_interface.cc
@@ -52,6 +52,7 @@ #include "./centipede/centipede_callbacks.h" #include "./centipede/command.h" #include "./centipede/coverage.h" +#include "./centipede/crash_summary.h" #include "./centipede/distill.h" #include "./centipede/environment.h" #include "./centipede/minimize_crash.h" @@ -292,7 +293,7 @@ // signatures of the remaining crashes. absl::flat_hash_set<std::string> PruneOldCrashesAndGetRemainingCrashSignatures( const std::filesystem::path &crashing_dir, const Environment &env, - CentipedeCallbacksFactory &callbacks_factory) { + CentipedeCallbacksFactory &callbacks_factory, CrashSummary &crash_summary) { const std::vector<std::string> crashing_input_files = // The corpus database layout assumes the crash input files are located // directly in the crashing subdirectory, so we don't list recursively. @@ -313,6 +314,11 @@ if (!is_reproducible || batch_result.IsSetupFailure() || is_duplicate) { CHECK_OK(RemotePathDelete(crashing_input_file, /*recursively=*/false)); } else { + crash_summary.AddCrash( + {std::filesystem::path(crashing_input_file).filename(), + /*category=*/batch_result.failure_description(), + batch_result.failure_signature(), + batch_result.failure_description()}); CHECK_OK(RemotePathTouchExistingFile(crashing_input_file)); } } @@ -322,7 +328,8 @@ // TODO(b/405382531): Add unit tests once the function is unit-testable. void DeduplicateAndStoreNewCrashes( const std::filesystem::path &crashing_dir, const WorkDir &workdir, - size_t total_shards, absl::flat_hash_set<std::string> crash_signatures) { + size_t total_shards, absl::flat_hash_set<std::string> crash_signatures, + CrashSummary &crash_summary) { for (size_t shard_idx = 0; shard_idx < total_shards; ++shard_idx) { const std::vector<std::string> new_crashing_input_files = // The crash reproducer directory may contain subdirectories with @@ -352,6 +359,24 @@ const bool is_duplicate = !crash_signatures.insert(new_crash_signature).second; if (is_duplicate) continue; + + const std::string crash_description_path = + crash_metadata_dir / absl::StrCat(crashing_input_file_name, ".desc"); + std::string new_crash_description; + const absl::Status description_status = + RemoteFileGetContents(crash_description_path, new_crash_description); + if (!description_status.ok()) { + LOG(WARNING) + << "Failed to read crash description for " + << crashing_input_file_name + << ". Will use the crash signature as the description. Status: " + << description_status; + new_crash_description = new_crash_signature; + } + crash_summary.AddCrash({crashing_input_file_name, + /*category=*/new_crash_description, + std::move(new_crash_signature), + new_crash_description}); CHECK_OK( RemoteFileRename(crashing_input_file, (crashing_dir / crashing_input_file_name).c_str())); @@ -666,12 +691,15 @@ } // Deduplicate and update the crashing inputs. + CrashSummary crash_summary{fuzztest_config.binary_identifier, + fuzz_tests_to_run[i]}; const std::filesystem::path crashing_dir = fuzztest_db_path / "crashing"; absl::flat_hash_set<std::string> crash_signatures = - PruneOldCrashesAndGetRemainingCrashSignatures(crashing_dir, env, - callbacks_factory); + PruneOldCrashesAndGetRemainingCrashSignatures( + crashing_dir, env, callbacks_factory, crash_summary); DeduplicateAndStoreNewCrashes(crashing_dir, workdir, env.total_shards, - std::move(crash_signatures)); + std::move(crash_signatures), crash_summary); + crash_summary.Report(&std::cerr); } return EXIT_SUCCESS;
diff --git a/centipede/crash_summary.cc b/centipede/crash_summary.cc new file mode 100644 index 0000000..8dbf313 --- /dev/null +++ b/centipede/crash_summary.cc
@@ -0,0 +1,46 @@ +// Copyright 2025 The Centipede Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "./centipede/crash_summary.h" + +#include <utility> + +#include "absl/strings/str_format.h" +#include "./centipede/util.h" +#include "./common/defs.h" + +namespace fuzztest::internal { + +void CrashSummary::AddCrash(Crash crash) { + crashes_.push_back(std::move(crash)); +} + +void CrashSummary::Report(absl::FormatRawSink sink) const { + absl::Format(sink, "=== Summary of detected crashes ===\n\n"); + absl::Format(sink, "Binary ID : %s\n", binary_id()); + absl::Format(sink, "Fuzz test : %s\n", fuzz_test()); + absl::Format(sink, "Total crashes: %d\n\n", crashes().size()); + int i = 0; + for (const Crash& crash : crashes()) { + absl::Format(sink, "Crash #%d:\n", ++i); + absl::Format(sink, " Crash ID : %s\n", crash.id); + absl::Format(sink, " Category : %s\n", crash.category); + absl::Format(sink, " Signature : %s\n", + AsPrintableString(AsByteSpan(crash.signature), 32)); + absl::Format(sink, " Description: %s\n\n", crash.description); + } + absl::Format(sink, "=== End of summary of detected crashes ===\n\n"); +} + +} // namespace fuzztest::internal
diff --git a/centipede/crash_summary.h b/centipede/crash_summary.h new file mode 100644 index 0000000..b5c631d --- /dev/null +++ b/centipede/crash_summary.h
@@ -0,0 +1,76 @@ +// Copyright 2025 The Centipede Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef FUZZTEST_CENTIPEDE_CRASH_SUMMARY_H_ +#define FUZZTEST_CENTIPEDE_CRASH_SUMMARY_H_ + +#include <string> +#include <string_view> +#include <vector> + +#include "absl/strings/str_format.h" +#include "absl/types/span.h" + +namespace fuzztest::internal { + +// Accumulates crashes for a single fuzz test and provides a method to report a +// summary of the crashes. +class CrashSummary { + public: + struct Crash { + std::string id; + std::string category; + std::string signature; + std::string description; + + friend bool operator==(const Crash& lhs, const Crash& rhs) { + return lhs.id == rhs.id && lhs.category == rhs.category && + lhs.signature == rhs.signature && + lhs.description == rhs.description; + } + }; + + explicit CrashSummary(std::string_view binary_id, std::string_view fuzz_test) + : binary_id_(std::string(binary_id)), + fuzz_test_(std::string(fuzz_test)) {} + + CrashSummary(const CrashSummary&) = default; + CrashSummary& operator=(const CrashSummary&) = default; + CrashSummary(CrashSummary&&) = default; + CrashSummary& operator=(CrashSummary&&) = default; + + // Adds a crash to the summary. + void AddCrash(Crash crash); + + // Reports a summary of the crashes to `sink`. + void Report(absl::FormatRawSink sink) const; + + std::string_view binary_id() const { return binary_id_; } + std::string_view fuzz_test() const { return fuzz_test_; } + absl::Span<const Crash> crashes() const { return crashes_; } + + friend bool operator==(const CrashSummary& lhs, const CrashSummary& rhs) { + return lhs.binary_id_ == rhs.binary_id_ && + lhs.fuzz_test_ == rhs.fuzz_test_ && lhs.crashes_ == rhs.crashes_; + } + + private: + std::string binary_id_; + std::string fuzz_test_; + std::vector<Crash> crashes_; +}; + +} // namespace fuzztest::internal + +#endif // FUZZTEST_CENTIPEDE_CRASH_SUMMARY_H_
diff --git a/centipede/crash_summary_test.cc b/centipede/crash_summary_test.cc new file mode 100644 index 0000000..1656a42 --- /dev/null +++ b/centipede/crash_summary_test.cc
@@ -0,0 +1,54 @@ +// Copyright 2025 The Centipede Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "./centipede/crash_summary.h" + +#include <string> +#include <string_view> + +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +namespace fuzztest::internal { +namespace { + +using ::testing::AllOf; +using ::testing::HasSubstr; + +TEST(CrashSummaryTest, ReportPrintsSummary) { + CrashSummary summary("binary_id", "fuzz_test"); + summary.AddCrash({"id1", "category1", "signature1", "description1"}); + summary.AddCrash({"id2", "category2", + "Unprintable (\xbe\xef) and very long signature", + "description2"}); + std::string output; + summary.Report(&output); + + EXPECT_THAT( + output, + AllOf(HasSubstr("Binary ID : binary_id"), + HasSubstr("Fuzz test : fuzz_test"), + HasSubstr("Total crashes: 2"), // + HasSubstr("Crash ID : id1"), // + HasSubstr("Category : category1"), + HasSubstr("Signature : signature1"), + HasSubstr("Description: description1"), + HasSubstr("Crash ID : id2"), // + HasSubstr("Category : category2"), + HasSubstr("Signature : Unprintable (\\xBE\\xEF) and very long s"), + HasSubstr("Description: description2"))); +} + +} // namespace +} // namespace fuzztest::internal
diff --git a/e2e_tests/corpus_database_test.cc b/e2e_tests/corpus_database_test.cc index 98422cd..6a2ed8d 100644 --- a/e2e_tests/corpus_database_test.cc +++ b/e2e_tests/corpus_database_test.cc
@@ -195,6 +195,20 @@ Contains(HasSubstr("FuzzTest.FailsInTwoWays/crashing/")).Times(2)); } +TEST_P(UpdateCorpusDatabaseTest, ReportsCrashSummary) { + EXPECT_THAT(GetUpdateCorpusDatabaseStdErr(), + AllOf(ContainsRegex( + R"re((?s)=== Summary of detected crashes === +.*?Fuzz test : FuzzTest.FailsInTwoWays +.*?Total crashes: 2 +.*?=== End of summary of detected crashes ===)re"), + ContainsRegex( + R"re((?s)=== Summary of detected crashes === +.*?Fuzz test : FuzzTest.FailsWithStackOverflow +.*?Total crashes: 1 +.*?=== End of summary of detected crashes ===)re"))); +} + TEST_P(UpdateCorpusDatabaseTest, StartsNewFuzzTestRunsWithoutExecutionIds) { TempDir corpus_database;