Skip to content

Internal change. #1755

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
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
25 changes: 24 additions & 1 deletion centipede/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -815,6 +815,7 @@ cc_library(
":centipede_lib",
":command",
":coverage",
":crash_summary",
":distill",
":environment",
":minimize_crash",
Expand Down Expand Up @@ -846,7 +847,9 @@ cc_library(
"@com_google_fuzztest//common:remote_file",
"@com_google_fuzztest//common:status_macros",
"@com_google_fuzztest//fuzztest/internal:configuration",
],
] + select({
"//conditions:default": [],
}),
)

cc_library(
Expand Down Expand Up @@ -951,6 +954,16 @@ cc_library(
],
)

cc_library(
name = "crash_summary",
srcs = ["crash_summary.cc"],
hdrs = ["crash_summary.h"],
deps = [
"@abseil-cpp//absl/strings:str_format",
"@abseil-cpp//absl/types:span",
],
)

cc_library(
name = "weak_sancov_stubs",
srcs = ["weak_sancov_stubs.cc"],
Expand Down Expand Up @@ -1840,6 +1853,16 @@ cc_test(
],
)

cc_test(
name = "crash_summary_test",
srcs = ["crash_summary_test.cc"],
deps = [
":crash_summary",
"@abseil-cpp//absl/log:check",
"@googletest//:gtest_main",
],
)

################################################################################
# Other tests
################################################################################
Expand Down
36 changes: 31 additions & 5 deletions centipede/centipede_interface.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -292,7 +293,7 @@ TestShard SetUpTestSharding() {
// 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.
Expand All @@ -313,6 +314,10 @@ absl::flat_hash_set<std::string> PruneOldCrashesAndGetRemainingCrashSignatures(
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(),
batch_result.failure_signature(),
batch_result.failure_description()});
CHECK_OK(RemotePathTouchExistingFile(crashing_input_file));
}
}
Expand All @@ -322,7 +327,8 @@ absl::flat_hash_set<std::string> PruneOldCrashesAndGetRemainingCrashSignatures(
// 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
Expand Down Expand Up @@ -352,6 +358,23 @@ void DeduplicateAndStoreNewCrashes(
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,
std::move(new_crash_signature),
std::move(new_crash_description)});
CHECK_OK(
RemoteFileRename(crashing_input_file,
(crashing_dir / crashing_input_file_name).c_str()));
Expand Down Expand Up @@ -666,12 +689,15 @@ int UpdateCorpusDatabaseForFuzzTests(
}

// 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;
Expand Down
54 changes: 54 additions & 0 deletions centipede/crash_summary.cc
Original file line number Diff line number Diff line change
@@ -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 <utility>

#include "absl/strings/str_format.h"

namespace fuzztest::internal {
namespace {

ExternalCrashReporter external_crash_reporter = nullptr;

} // namespace

void CrashSummary::AddCrash(Crash crash) {
crashes_.push_back(std::move(crash));
}

void CrashSummary::Report(absl::FormatRawSink sink) const {
if (external_crash_reporter != nullptr) {
external_crash_reporter(*this);
}
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, " Signature : %s\n", crash.signature);
absl::Format(sink, " Description: %s\n\n", crash.description);
}
absl::Format(sink, "=== End of summary ===\n\n");
}

void SetExternalCrashReporter(ExternalCrashReporter reporter) {
external_crash_reporter = reporter;
}

} // namespace fuzztest::internal
82 changes: 82 additions & 0 deletions centipede/crash_summary.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// 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 signature;
std::string description;

friend bool operator==(const Crash& lhs, const Crash& rhs) {
return lhs.id == rhs.id && 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`. If an external crash reporter
// has been set with `SetExternalCrashReporter`, calls it with the stored
// crashes.
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_;
};

using ExternalCrashReporter = void (*)(const CrashSummary&);

// Sets an external crash reporter that will be called when a `CrashSummary`
// is reported.
void SetExternalCrashReporter(ExternalCrashReporter reporter);

} // namespace fuzztest::internal

#endif // FUZZTEST_CENTIPEDE_CRASH_SUMMARY_H_
81 changes: 81 additions & 0 deletions centipede/crash_summary_test.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// 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"
#include "absl/log/check.h"

namespace fuzztest::internal {
namespace {

using ::testing::AllOf;
using ::testing::HasSubstr;
using ::testing::Pointee;

class CrashSummaryTest : public testing::Test {
public:
~CrashSummaryTest() {
if (dumped_summary_ != nullptr) {
delete dumped_summary_;
dumped_summary_ = nullptr;
}
}

protected:
static void DumpCrashSummary(const CrashSummary& summary) {
CHECK(dumped_summary_ == nullptr);
dumped_summary_ = new CrashSummary{summary};
};

static CrashSummary* dumped_summary_;
};

CrashSummary* CrashSummaryTest::dumped_summary_ = nullptr;

TEST_F(CrashSummaryTest, ReportPrintsSummary) {
CrashSummary summary("binary_id", "fuzz_test");
summary.AddCrash({"id1", "signature1", "description1"});
summary.AddCrash({"id2", "signature2", "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("Signature : signature1"),
HasSubstr("Description: description1"),
HasSubstr("Crash ID : id2"), //
HasSubstr("Signature : signature2"),
HasSubstr("Description: description2")));
}

TEST_F(CrashSummaryTest, ReportCallsExternalCrashReporter) {
CrashSummary summary("binary_id", "fuzz_test");
summary.AddCrash({"id1", "signature1", "description1"});
summary.AddCrash({"id2", "signature2", "description2"});
SetExternalCrashReporter(DumpCrashSummary);
std::string output;
summary.Report(&output);

EXPECT_THAT(dumped_summary_, Pointee(summary));
}

} // namespace
} // namespace fuzztest::internal
14 changes: 14 additions & 0 deletions e2e_tests/corpus_database_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,20 @@ TEST_P(UpdateCorpusDatabaseTest, DeduplicatesCrashes) {
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 ===)re"),
ContainsRegex(
R"re((?s)=== Summary of detected crashes ===
.*?Fuzz test : FuzzTest.FailsWithStackOverflow
.*?Total crashes: 1
.*?=== End of summary ===)re")));
}

TEST_P(UpdateCorpusDatabaseTest, StartsNewFuzzTestRunsWithoutExecutionIds) {
TempDir corpus_database;

Expand Down