Handle empty crash signatures and descriptions in crash deduplication. This change adds checks for empty crash signatures and descriptions. By default, inputs with empty metadata are now ignored with an error log. An environment variable, FUZZTEST_FAIL_ON_EMPTY_CRASH_METADATA, can be set to make these empty metadata cases fatal. Additionally, failures to read crash descriptions now cause the corresponding input to be ignored. PiperOrigin-RevId: 868929414
diff --git a/centipede/crash_deduplication.cc b/centipede/crash_deduplication.cc index 417a0b6..545eb2d 100644 --- a/centipede/crash_deduplication.cc +++ b/centipede/crash_deduplication.cc
@@ -15,6 +15,7 @@ #include "./centipede/crash_deduplication.h" #include <cstddef> +#include <cstdlib> #include <filesystem> // NOLINT #include <string> #include <string_view> @@ -53,6 +54,8 @@ absl::flat_hash_map<std::string, CrashDetails> GetCrashesFromWorkdir( const WorkDir& workdir, size_t total_shards) { + const bool fail_on_empty_crash_metadata = + std::getenv("FUZZTEST_FAIL_ON_EMPTY_CRASH_METADATA") != nullptr; absl::flat_hash_map<std::string, CrashDetails> crashes; for (size_t shard_idx = 0; shard_idx < total_shards; ++shard_idx) { std::vector<std::string> crashing_input_paths = @@ -79,6 +82,15 @@ << " due to failure to read the crash signature: " << status; continue; } + if (crash_signature.empty()) { + FUZZTEST_LOG_IF(FATAL, fail_on_empty_crash_metadata) + << "Empty crash signature for " << crashing_input_file_name; + FUZZTEST_LOG(ERROR) + << "Ignoring crashing input " << crashing_input_file_name + << " due to empty crash signature. This is an internal error; " + "please report it to the FuzzTest team!"; + continue; + } if (crashes.contains(crash_signature)) continue; const std::string crash_description_path = @@ -86,9 +98,22 @@ std::string crash_description; const absl::Status description_status = RemoteFileGetContents(crash_description_path, crash_description); - FUZZTEST_LOG_IF(WARNING, !description_status.ok()) - << "Failed to read crash description for " << crashing_input_file_name - << ".Status: " << description_status; + if (!description_status.ok()) { + FUZZTEST_LOG(WARNING) + << "Ignoring crashing input " << crashing_input_file_name + << " due to failure to read the crash description: " + << description_status; + continue; + } + if (crash_description.empty()) { + FUZZTEST_LOG_IF(FATAL, fail_on_empty_crash_metadata) + << "Empty crash description for " << crashing_input_file_name; + FUZZTEST_LOG(ERROR) + << "Ignoring crashing input " << crashing_input_file_name + << " due to empty crash description. This is an internal error; " + "please report it to the FuzzTest team!"; + continue; + } crashes.insert( {std::move(crash_signature), // Centipede uses the input signature (i.e., the hash of the input)
diff --git a/centipede/crash_deduplication_test.cc b/centipede/crash_deduplication_test.cc index e73e7c5..bd1966e 100644 --- a/centipede/crash_deduplication_test.cc +++ b/centipede/crash_deduplication_test.cc
@@ -91,6 +91,16 @@ // `isig4` lacks `.sig` and `.desc` files and should be ignored. SetContentsAndGetPath(crashes1, "isig4", "input4"); + // `isig5` has empty crash signature and should be ignored. + auto input5_path = SetContentsAndGetPath(crashes1, "isig5", "input5"); + SetContentsAndGetPath(crash_metadata1, "isig5.sig", ""); + SetContentsAndGetPath(crash_metadata1, "isig5.desc", "desc5"); + + // `isig6` has empty crash description and should be ignored. + auto input6_path = SetContentsAndGetPath(crashes1, "isig6", "input6"); + SetContentsAndGetPath(crash_metadata1, "isig6.sig", "csig6"); + SetContentsAndGetPath(crash_metadata1, "isig6.desc", ""); + const auto crashes = GetCrashesFromWorkdir(workdir, /*total_shards=*/2); EXPECT_THAT( crashes, @@ -100,6 +110,52 @@ Pair("csig2", FieldsAre("isig2", "desc2", input2_path)))); } +TEST(GetCrashesFromWorkdirTest, FailsOnEmptyCrashSignatureIfEnvVarSet) { + TempDir test_dir; + const std::string workdir_path = test_dir.path(); + WorkDir workdir{workdir_path, "binary_name", "binary_hash", + /*my_shard_index=*/0}; + + const std::filesystem::path crashes = + workdir.CrashReproducerDirPaths().Shard(0); + const std::filesystem::path crash_metadata = + workdir.CrashMetadataDirPaths().Shard(0); + std::filesystem::create_directories(crashes); + std::filesystem::create_directories(crash_metadata); + + auto input_path = SetContentsAndGetPath(crashes, "isig", "input"); + SetContentsAndGetPath(crash_metadata, "isig.sig", ""); + SetContentsAndGetPath(crash_metadata, "isig.desc", "desc"); + + setenv("FUZZTEST_FAIL_ON_EMPTY_CRASH_METADATA", "1", /*overwrite=*/1); + EXPECT_DEATH(GetCrashesFromWorkdir(workdir, /*total_shards=*/1), + "Empty crash signature"); + unsetenv("FUZZTEST_FAIL_ON_EMPTY_CRASH_METADATA"); +} + +TEST(GetCrashesFromWorkdirTest, FailsOnEmptyCrashDescriptionIfEnvVarSet) { + TempDir test_dir; + const std::string workdir_path = test_dir.path(); + WorkDir workdir{workdir_path, "binary_name", "binary_hash", + /*my_shard_index=*/0}; + + const std::filesystem::path crashes = + workdir.CrashReproducerDirPaths().Shard(0); + const std::filesystem::path crash_metadata = + workdir.CrashMetadataDirPaths().Shard(0); + std::filesystem::create_directories(crashes); + std::filesystem::create_directories(crash_metadata); + + auto input_path = SetContentsAndGetPath(crashes, "isig", "input"); + SetContentsAndGetPath(crash_metadata, "isig.sig", "csig"); + SetContentsAndGetPath(crash_metadata, "isig.desc", ""); + + setenv("FUZZTEST_FAIL_ON_EMPTY_CRASH_METADATA", "1", /*overwrite=*/1); + EXPECT_DEATH(GetCrashesFromWorkdir(workdir, /*total_shards=*/1), + "Empty crash description"); + unsetenv("FUZZTEST_FAIL_ON_EMPTY_CRASH_METADATA"); +} + class FakeCentipedeCallbacks : public CentipedeCallbacks { public: struct Crash {