// Copyright 2022 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/coverage.h" #include #include #include #include #include #include // NOLINT #include #include #include #include #include "gmock/gmock.h" #include "gtest/gtest.h" #include "absl/container/flat_hash_set.h" #include "./centipede/binary_info.h" #include "./centipede/control_flow.h" #include "./centipede/environment.h" #include "./centipede/feature.h" #include "./centipede/pc_info.h" #include "./centipede/symbol_table.h" #include "./centipede/test_coverage_util.h" #include "./centipede/thread_pool.h" #include "./centipede/util.h" #include "./common/test_util.h" namespace fuzztest::internal { namespace { // llvm-symbolizer output for a binary with 3 functions: // A, BB, CCC. // A and BB have one control flow edge each. // CCC has 3 edges. const char *symbolizer_output = "A\n" "a.cc:1:0\n" "\n" "BB\n" "bb.cc:1:0\n" "\n" "CCC\n" "ccc.cc:1:0\n" "\n" "CCC\n" "ccc.cc:2:0\n" "\n" "CCC\n" "ccc.cc:3:0\n" "\n" "CCC\n" "ccc.cc:3:0\n" // same as the previous entry "\n"; // PCTable that corresponds to symbolizer_output above. static const PCTable g_pc_table = { {100, PCInfo::kFuncEntry}, {200, PCInfo::kFuncEntry}, {300, PCInfo::kFuncEntry}, {400, 0}, {500, 0}, {600, 0}, }; // Tests Coverage and SymbolTable together. TEST(Coverage, SymbolTable) { const std::filesystem::path test_dir = GetTestTempDir(test_info_->name()); // Initialize and test SymbolTable. SymbolTable symbols; std::istringstream iss(symbolizer_output); symbols.ReadFromLLVMSymbolizer(iss); EXPECT_EQ(symbols.size(), 6U); EXPECT_EQ(symbols.func(1), "BB"); EXPECT_EQ(symbols.location(2), "ccc.cc:1:0"); EXPECT_EQ(symbols.full_description(0), "A a.cc:1:0"); EXPECT_EQ(symbols.full_description(4), "CCC ccc.cc:3:0"); { // Tests coverage output for PCIndexVec = {0, 2}, // i.e. the covered edges are 'A' and the entry of 'CCC'. Coverage cov(g_pc_table, {0, 2}); cov.DumpReportToFile(symbols, (test_dir / "coverage.txt").string()); std::string str; ReadFromLocalFile((test_dir / "coverage.txt").string(), str); EXPECT_THAT(str, testing::HasSubstr("FULL: A a.cc:1:0")); EXPECT_THAT(str, testing::HasSubstr("NONE: BB bb.cc:1:0")); EXPECT_THAT(str, testing::HasSubstr("PARTIAL: CCC ccc.cc:1:0")); EXPECT_THAT(str, testing::HasSubstr("+ CCC ccc.cc:1:0")); EXPECT_THAT(str, testing::HasSubstr("- CCC ccc.cc:2:0")); EXPECT_THAT(str, testing::HasSubstr("- CCC ccc.cc:3:0")); } { // Same as above, but for PCIndexVec = {1, 2, 3}, Coverage cov(g_pc_table, {1, 2, 3}); cov.DumpReportToFile(symbols, (test_dir / "coverage.txt").string()); std::string str; ReadFromLocalFile((test_dir / "coverage.txt").string(), str); EXPECT_THAT(str, testing::HasSubstr("FULL: BB bb.cc:1:0")); EXPECT_THAT(str, testing::HasSubstr("NONE: A a.cc:1:0")); EXPECT_THAT(str, testing::HasSubstr("PARTIAL: CCC ccc.cc:1:0")); EXPECT_THAT(str, testing::HasSubstr("+ CCC ccc.cc:1:0")); EXPECT_THAT(str, testing::HasSubstr("+ CCC ccc.cc:2:0")); EXPECT_THAT(str, testing::HasSubstr("- CCC ccc.cc:3:0")); } symbols.SetAllToUnknown(2); EXPECT_EQ(symbols.size(), 2); EXPECT_EQ(symbols.full_description(0), "? ?"); EXPECT_EQ(symbols.full_description(1), "? ?"); } TEST(Coverage, CoverageLoad) { Coverage cov(g_pc_table, {0, 2, 4, 5}); EXPECT_TRUE(cov.BlockIsCovered(0)); EXPECT_FALSE(cov.BlockIsCovered(1)); EXPECT_TRUE(cov.BlockIsCovered(2)); EXPECT_FALSE(cov.BlockIsCovered(3)); EXPECT_TRUE(cov.BlockIsCovered(4)); EXPECT_TRUE(cov.BlockIsCovered(5)); EXPECT_TRUE(cov.FunctionIsFullyCovered(0)); EXPECT_FALSE(cov.FunctionIsFullyCovered(1)); EXPECT_FALSE(cov.FunctionIsFullyCovered(2)); } TEST(Coverage, CoverageLogger) { SymbolTable symbols; std::istringstream iss(symbolizer_output); symbols.ReadFromLLVMSymbolizer(iss); CoverageLogger logger(g_pc_table, symbols); // First time logging pc_index=0. EXPECT_EQ(logger.ObserveAndDescribeIfNew(0), "FUNC: A a.cc:1:0"); // Second time logger pc_index=0. EXPECT_EQ(logger.ObserveAndDescribeIfNew(0), ""); // First time logging pc_index=4. EXPECT_EQ(logger.ObserveAndDescribeIfNew(4), "EDGE: CCC ccc.cc:3:0"); // First time logging pc_index=5, but it produces the same description as // pc_index=4, and so the result is empty. EXPECT_EQ(logger.ObserveAndDescribeIfNew(5), ""); // Logging with pc_index out of bounds. Second time gives empty result. EXPECT_EQ(logger.ObserveAndDescribeIfNew(42), "FUNC/EDGE index: 42"); EXPECT_EQ(logger.ObserveAndDescribeIfNew(42), ""); CoverageLogger concurrently_used_logger(g_pc_table, symbols); auto cb = [&]() { for (int i = 0; i < 1000; i++) { PCIndex pc_index = i % g_pc_table.size(); logger.ObserveAndDescribeIfNew(pc_index); } }; { ThreadPool threads{2}; threads.Schedule(cb); threads.Schedule(cb); } // The threads join here. } // Returns path to test_fuzz_target. static std::string GetTargetPath() { return GetDataDependencyFilepath("centipede/testing/test_fuzz_target"); } // Returns path to threaded_fuzz_target. static std::string GetThreadedTargetPath() { return GetDataDependencyFilepath("centipede/testing/threaded_fuzz_target"); } // Tests coverage collection on test_fuzz_target // using two inputs that trigger different code paths. TEST(Coverage, CoverageFeatures) { // Prepare the inputs. Environment env; env.binary = GetTargetPath(); auto features = RunInputsAndCollectCoverage(env, {"func1", "func2-A"}); EXPECT_EQ(features.size(), 2); EXPECT_NE(features[0], features[1]); // Get pc_table and symbols. bool uses_legacy_trace_pc_instrumentation = {}; BinaryInfo binary_info; binary_info.InitializeFromSanCovBinary( GetTargetPath(), GetObjDumpPath(), GetLLVMSymbolizerPath(), GetTestTempDir(test_info_->name()).string()); const auto &pc_table = binary_info.pc_table; EXPECT_FALSE(uses_legacy_trace_pc_instrumentation); const SymbolTable &symbols = binary_info.symbols; // pc_table and symbols should have the same size. EXPECT_EQ(pc_table.size(), symbols.size()); // Check what's covered. // Both inputs should cover LLVMFuzzerTestOneInput. // Input[0] should cover SingleEdgeFunc and not MultiEdgeFunc. // Input[1] - the other way around. for (size_t input_idx = 0; input_idx < 2; input_idx++) { size_t llvm_fuzzer_test_one_input_num_edges = 0; size_t single_edge_func_num_edges = 0; size_t multi_edge_func_num_edges = 0; for (auto feature : features[input_idx]) { if (!feature_domains::kPCs.Contains(feature)) continue; auto pc_index = ConvertPCFeatureToPcIndex(feature); single_edge_func_num_edges += symbols.func(pc_index) == "SingleEdgeFunc"; multi_edge_func_num_edges += symbols.func(pc_index) == "MultiEdgeFunc"; llvm_fuzzer_test_one_input_num_edges += symbols.func(pc_index) == "LLVMFuzzerTestOneInput"; } EXPECT_GT(llvm_fuzzer_test_one_input_num_edges, 1); if (input_idx == 0) { // This input calls SingleEdgeFunc, but not MultiEdgeFunc. EXPECT_EQ(single_edge_func_num_edges, 1); EXPECT_EQ(multi_edge_func_num_edges, 0); } else { // This input calls MultiEdgeFunc, but not SingleEdgeFunc. EXPECT_EQ(single_edge_func_num_edges, 0); EXPECT_GT(multi_edge_func_num_edges, 1); } } } static FeatureVec ExtractDomainFeatures(const FeatureVec &features, const feature_domains::Domain &domain) { FeatureVec result; for (auto feature : features) { if (domain.Contains(feature)) { result.push_back(feature); } } return result; } // Tests data flow instrumentation and feature collection. TEST(Coverage, DataFlowFeatures) { Environment env; env.binary = GetTargetPath(); auto features_g = RunInputsAndCollectCoverage(env, {"glob1", "glob2"}); auto features_c = RunInputsAndCollectCoverage(env, {"cons1", "cons2"}); for (auto &features : {features_g, features_c}) { EXPECT_EQ(features.size(), 2); // Dataflow features should be different. EXPECT_NE(ExtractDomainFeatures(features[0], feature_domains::kDataFlow), ExtractDomainFeatures(features[1], feature_domains::kDataFlow)); // But control flow features should be the same. EXPECT_EQ( ExtractDomainFeatures(features[0], feature_domains::k8bitCounters), ExtractDomainFeatures(features[1], feature_domains::k8bitCounters)); } } // Tests feature collection for counters (--use_counter_features). TEST(Coverage, CounterFeatures) { Environment env; env.binary = GetTargetPath(); // Inputs that generate the same PC coverage but different counters. std::vector inputs = {"cnt\x01", "cnt\x02", "cnt\x04", "cnt\x08", "cnt\x10"}; const size_t n = inputs.size(); // Run with use_counter_features = true. env.use_counter_features = true; auto features = RunInputsAndCollectCoverage(env, inputs); EXPECT_EQ(features.size(), n); // Counter features should be different. for (size_t i = 0; i < n; ++i) { for (size_t j = i + 1; j < n; ++j) { EXPECT_NE( ExtractDomainFeatures(features[i], feature_domains::k8bitCounters), ExtractDomainFeatures(features[j], feature_domains::k8bitCounters)); } } // Run with use_counter_features = false. env.use_counter_features = false; features = RunInputsAndCollectCoverage(env, inputs); EXPECT_EQ(features.size(), n); // Counter features should be the same now. for (size_t i = 0; i < n; ++i) { for (size_t j = i + 1; j < n; ++j) { EXPECT_EQ( ExtractDomainFeatures(features[i], feature_domains::k8bitCounters), ExtractDomainFeatures(features[j], feature_domains::k8bitCounters)); } } } // For each of {ABToCmpModDiff, ABToCmpHamming, ABToCmpDiffLog} verify that // a) they create all possible values in [0,64) // b) they don't create any other values. // c) they are sufficiently different from each other, i.e. not using one of // them as coverage signal may reduce the overall quality of signal. TEST(Coverage, CMPFeatures) { absl::flat_hash_set moddiff, hamming, difflog; // clear all hash sets. auto clear = [&]() { moddiff.clear(); hamming.clear(); difflog.clear(); }; // verifies `value` < 64 and returns it. auto must_be_6bit = [](uintptr_t value) { EXPECT_LT(value, 64); return value; }; // inserts a value into all hash sets. auto update = [&](uintptr_t a, uintptr_t b) { moddiff.insert(must_be_6bit(ABToCmpModDiff(a, b))); hamming.insert(must_be_6bit(ABToCmpHamming(a, b))); difflog.insert(must_be_6bit(ABToCmpDiffLog(a, b))); }; // Check moddiff. clear(); for (uintptr_t a = 0; a <= 64; ++a) { uintptr_t b = 32; if (a == b) continue; update(a, b); } EXPECT_EQ(moddiff.size(), 64); EXPECT_EQ(hamming.size(), 6); EXPECT_EQ(difflog.size(), 6); // Check hamming. clear(); for (uintptr_t bits = 0; bits < 64; ++bits) { uintptr_t minus_one = -1; uintptr_t a = minus_one << bits; update(a, 0); } EXPECT_EQ(moddiff.size(), 6); EXPECT_EQ(hamming.size(), 64); EXPECT_EQ(difflog.size(), 1); // Check difflog. clear(); for (uintptr_t bits = 0; bits < 64; ++bits) { uintptr_t a = 1ULL << bits; uintptr_t b = 0; update(a, b); } EXPECT_EQ(moddiff.size(), 7); EXPECT_EQ(hamming.size(), 1); EXPECT_EQ(difflog.size(), 64); } // Tests CMP tracing and feature collection. TEST(Coverage, CMPFeaturesExecute) { Environment env; env.binary = GetTargetPath(); auto features = RunInputsAndCollectCoverage(env, {"cmpAAAAAAAA", "cmpAAAABBBB"}); EXPECT_EQ(features.size(), 2); // CMP features should be different. EXPECT_NE(ExtractDomainFeatures(features[0], feature_domains::kCMPEq), ExtractDomainFeatures(features[1], feature_domains::kCMPEq)); EXPECT_NE(ExtractDomainFeatures(features[0], feature_domains::kCMPModDiff), ExtractDomainFeatures(features[1], feature_domains::kCMPModDiff)); EXPECT_NE(ExtractDomainFeatures(features[0], feature_domains::kCMPHamming), ExtractDomainFeatures(features[1], feature_domains::kCMPHamming)); EXPECT_NE(ExtractDomainFeatures(features[0], feature_domains::kCMPDiffLog), ExtractDomainFeatures(features[1], feature_domains::kCMPDiffLog)); // But control flow features should be the same. EXPECT_EQ(ExtractDomainFeatures(features[0], feature_domains::k8bitCounters), ExtractDomainFeatures(features[1], feature_domains::k8bitCounters)); } // Tests memcmp interceptor. TEST(Coverage, CMPFeaturesFromMemcmp) { Environment env; env.binary = GetTargetPath(); auto features = RunInputsAndCollectCoverage(env, {"mcmpAAAAAAAA", "mcmpAAAABBBB"}); EXPECT_EQ(features.size(), 2); // CMP features should be different. EXPECT_NE(ExtractDomainFeatures(features[0], feature_domains::kCMP), ExtractDomainFeatures(features[1], feature_domains::kCMP)); // But control flow features should be the same. EXPECT_EQ(ExtractDomainFeatures(features[0], feature_domains::k8bitCounters), ExtractDomainFeatures(features[1], feature_domains::k8bitCounters)); } TEST(Coverage, PathFeatures) { Environment env; env.binary = GetTargetPath(); env.path_level = 10; // Inputs "pth123" and "pth321" generate different call sequences but exactly // the same edge coverage. This test verifies that we can capture this. auto features = RunInputsAndCollectCoverage(env, {"pth123", "pth321"}); EXPECT_EQ(features.size(), 2); // Path features should be different. EXPECT_NE(ExtractDomainFeatures(features[0], feature_domains::kBoundedPath), ExtractDomainFeatures(features[1], feature_domains::kBoundedPath)); // But control flow features should be the same. EXPECT_EQ(ExtractDomainFeatures(features[0], feature_domains::k8bitCounters), ExtractDomainFeatures(features[1], feature_domains::k8bitCounters)); } TEST(Coverage, FunctionFilter) { // Initialize coverage data. BinaryInfo binary_info; binary_info.InitializeFromSanCovBinary( GetTargetPath(), GetObjDumpPath(), GetLLVMSymbolizerPath(), GetTestTempDir(test_info_->name()).string()); const PCTable &pc_table = binary_info.pc_table; EXPECT_FALSE(binary_info.uses_legacy_trace_pc_instrumentation); const DsoTable dso_table = {{GetTargetPath(), pc_table.size()}}; SymbolTable symbols; symbols.GetSymbolsFromBinary(pc_table, dso_table, GetLLVMSymbolizerPath(), GetTestTempDir(test_info_->name()).string()); // Empty filter. FunctionFilter empty_filter("", symbols); EXPECT_EQ(empty_filter.count(), 0); // Single-function filter. The function has one PC. FunctionFilter sing_edge_func_filter("SingleEdgeFunc", symbols); EXPECT_EQ(sing_edge_func_filter.count(), 1); // Another single-function filter. This function has several PCs. FunctionFilter multi_edge_func_filter("MultiEdgeFunc", symbols); EXPECT_GT(multi_edge_func_filter.count(), 1); // Two-function-filter. FunctionFilter both_func_filter("MultiEdgeFunc,SingleEdgeFunc", symbols); EXPECT_GT(both_func_filter.count(), multi_edge_func_filter.count()); // Collect features from the test target by running 3 different inputs. Environment env; env.binary = GetTargetPath(); std::vector features = RunInputsAndCollectCoverage(env, {"func1", "func2-A", "other"}); EXPECT_EQ(features.size(), 3); auto &single = features[0]; auto &multi = features[1]; auto &other = features[2]; // Check the features against the different filters. EXPECT_TRUE(empty_filter.filter(single)); EXPECT_TRUE(empty_filter.filter(multi)); EXPECT_TRUE(empty_filter.filter(other)); EXPECT_TRUE(sing_edge_func_filter.filter(single)); EXPECT_FALSE(sing_edge_func_filter.filter(multi)); EXPECT_FALSE(sing_edge_func_filter.filter(other)); EXPECT_FALSE(multi_edge_func_filter.filter(single)); EXPECT_TRUE(multi_edge_func_filter.filter(multi)); EXPECT_FALSE(multi_edge_func_filter.filter(other)); EXPECT_TRUE(both_func_filter.filter(single)); EXPECT_TRUE(both_func_filter.filter(multi)); EXPECT_FALSE(both_func_filter.filter(other)); } TEST(Coverage, ThreadedTest) { Environment env; env.path_level = 10; env.binary = GetThreadedTargetPath(); std::vector features = RunInputsAndCollectCoverage(env, {"f", "fu", "fuz", "fuzz"}); EXPECT_EQ(features.size(), 4); // For several pairs of inputs, check that their features in // kPC and kBoundedPath are different. for (size_t idx0 = 0; idx0 < 3; ++idx0) { for (size_t idx1 = idx0 + 1; idx1 < 4; ++idx1) { EXPECT_NE(ExtractDomainFeatures(features[idx0], feature_domains::kPCs), ExtractDomainFeatures(features[idx1], feature_domains::k8bitCounters)); EXPECT_NE( ExtractDomainFeatures(features[idx0], feature_domains::kBoundedPath), ExtractDomainFeatures(features[idx1], feature_domains::kBoundedPath)); } } } TEST(FrontierWeight, ComputeFrontierWeight) { PCTable g_pc_table{{0, PCInfo::kFuncEntry}, {1, PCInfo::kFuncEntry}, {2, 0}, {3, PCInfo::kFuncEntry}, {4, PCInfo::kFuncEntry}}; // A simple CF table, to get cyclomatic complexity of 1 for all functions. CFTable g_cf_table{ 0, 0, 0, 1, 0, 0, 2, 0, 0, 3, 0, 0, 4, 0, 0, }; Coverage g_coverage(g_pc_table, {0, 1}); ControlFlowGraph cfg; cfg.InitializeControlFlowGraph(g_cf_table, g_pc_table); std::vector callees1 = {0, 1, 3, 4}; std::vector callees2 = {0, 1}; std::vector callees3 = {0}; // PC 99 should have no effect on computed weight. std::vector callees4 = {1, 3, 99}; auto weight1 = ComputeFrontierWeight(g_coverage, cfg, callees1); ASSERT_EQ(weight1, 408); auto weight2 = ComputeFrontierWeight(g_coverage, cfg, callees2); ASSERT_EQ(weight2, 102); auto weight3 = ComputeFrontierWeight(g_coverage, cfg, callees3); ASSERT_EQ(weight3, 25); auto weight4 = ComputeFrontierWeight(g_coverage, cfg, callees4); ASSERT_EQ(weight4, 230); } TEST(FrontierWeightDeath, InvalidCallee) { // Makes call to ComputeFrontierWeight with some non-function PCs. PCTable g_pc_table{{0, PCInfo::kFuncEntry}, {1, 0}, {2, 0}}; CFTable g_cf_table{0, 1, 0, 0, 1, 2, 0, 0, 2, 0, 0}; Coverage g_coverage(g_pc_table, {0, 1}); ControlFlowGraph cfg; cfg.InitializeControlFlowGraph(g_cf_table, g_pc_table); EXPECT_DEATH(ComputeFrontierWeight(g_coverage, cfg, {0, 1}), ""); EXPECT_DEATH(ComputeFrontierWeight(g_coverage, cfg, {1, 2}), ""); } } // namespace } // namespace fuzztest::internal