Problem: SummaryStore keyed summaries by getQualifiedNameAsString (plus "@file" for internal linkage), which overloads share. When a TU calls a function whose body lives in another TU, the lookup returns whichever overload was summarized last. Example: `take(int*, int)` dereferences p, `take(int*, long)` checks it, and a call `take(q, 1L)` with a null q was reported as a null-deref because it inherited the other overload's nonnull precondition. Lookups also used a different spelling (no "@file") than stores for static functions, so their summaries were never found at call sites. Fix: summaryStoreKey()/summaryStoreKeyForCall() return the declaration's USR (semanticFunctionId: USR, TU-qualified for internal linkage). Every clang-side store, lookup, dependency edge and staleness check uses it: summary_encoder, null-deref, div-by-zero, OOB and the summary priming pipeline, which still selects targets by the call graph's qualified names. Rust summaries keep their own keys. Now that static callees' summaries are found, null-deref's summary-precondition path applies the same DEBUGASSERT and static-parameter-contract filters as its callee-deref path. Test: tests/unit/test_summary_key_identity.cpp covers per-overload summaries, key identity across redeclarations and distinct keys across namespaces, and the cross-TU overload call. That call reported a false null-deref before the fix; a control call to the dereferencing overload still reports one. Tests that hand-built the old keys (test_symbolic_summary, test_cross_tu_security) now derive them from the declaration. Full suite: 140/140 (rust_frontend's 500 ms timeout test flaked once at load average 22 and passed on four reruns).
721 lines
30 KiB
C++
721 lines
30 KiB
C++
#include <clang/AST/RecursiveASTVisitor.h>
|
|
#include <clang/Frontend/ASTUnit.h>
|
|
#include <clang/Tooling/CompilationDatabase.h>
|
|
#include <clang/Tooling/Tooling.h>
|
|
|
|
#include <algorithm>
|
|
#include <catch2/catch_test_macros.hpp>
|
|
#include <chrono>
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
#include <nlohmann/json.hpp>
|
|
#include <string>
|
|
#include <unordered_map>
|
|
#include <vector>
|
|
|
|
#include "fold/cli/incremental_fingerprint.hpp"
|
|
#include "fold/cli/scan_options.hpp"
|
|
#include "fold/cli/scan_router.hpp"
|
|
#include "fold/cli/scan_runner.hpp"
|
|
#include "fold/cli/source_call_graph.hpp"
|
|
#include "fold/cli/source_summary_pipeline.hpp"
|
|
#include "fold/detect/cross_tu_cache.hpp"
|
|
#include "fold/detect/incremental_cache.hpp"
|
|
#include "fold/detect/summary_store.hpp"
|
|
#include "fold/frontend/comp_db.hpp"
|
|
#include "fold/ir/call_graph.hpp"
|
|
#include "fold/symbolic/encoder_callee_names.hpp"
|
|
|
|
namespace {
|
|
|
|
struct TempDir {
|
|
std::filesystem::path path;
|
|
|
|
TempDir()
|
|
: path(std::filesystem::temp_directory_path() /
|
|
("fold-cross-tu-" +
|
|
std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()))) {
|
|
std::filesystem::create_directories(path);
|
|
}
|
|
|
|
~TempDir() { std::filesystem::remove_all(path); }
|
|
};
|
|
|
|
// SummaryStore key (USR-based) of the function `name` defined in `path`,
|
|
// computed from the same file the scan parsed.
|
|
std::string summaryKeyIn(const std::filesystem::path& path, const std::string& name) {
|
|
std::ifstream in(path);
|
|
const std::string code((std::istreambuf_iterator<char>(in)), std::istreambuf_iterator<char>());
|
|
auto unit = clang::tooling::buildASTFromCodeWithArgs(code, {"-std=c++20"}, path.string());
|
|
if (!unit)
|
|
return {};
|
|
struct Finder : clang::RecursiveASTVisitor<Finder> {
|
|
std::string name;
|
|
const clang::FunctionDecl* found = nullptr;
|
|
bool VisitFunctionDecl(clang::FunctionDecl* fd) {
|
|
if (!found && fd->getNameAsString() == name)
|
|
found = fd;
|
|
return true;
|
|
}
|
|
} finder;
|
|
finder.name = name;
|
|
finder.TraverseDecl(unit->getASTContext().getTranslationUnitDecl());
|
|
return fold::summaryStoreKey(finder.found);
|
|
}
|
|
|
|
void writeFile(const std::filesystem::path& path, const std::string& content) {
|
|
std::ofstream(path) << content;
|
|
}
|
|
|
|
void writeCompileDb(const std::filesystem::path& dir, const std::filesystem::path& caller,
|
|
const std::filesystem::path& callee) {
|
|
std::vector<std::filesystem::path> files{caller, callee};
|
|
nlohmann::json db = nlohmann::json::array();
|
|
for (const auto& file : files) {
|
|
db.push_back({
|
|
{"directory", dir.string()},
|
|
{"command", std::string("clang++ -std=c++20 -c ") + file.string()},
|
|
{"file", file.string()},
|
|
});
|
|
}
|
|
writeFile(dir / "compile_commands.json", db.dump());
|
|
}
|
|
|
|
const fold::detect::IncrementalFunctionState* findFingerprintByName(
|
|
const std::unordered_map<std::string, fold::detect::IncrementalFunctionState>& fingerprints,
|
|
const std::string& qualified_name) {
|
|
for (const auto& [_, state] : fingerprints) {
|
|
if (state.fingerprint.qualified_name == qualified_name)
|
|
return &state;
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
void writeCompileDb(const std::filesystem::path& dir,
|
|
const std::vector<std::filesystem::path>& files) {
|
|
nlohmann::json db = nlohmann::json::array();
|
|
for (const auto& file : files) {
|
|
db.push_back({
|
|
{"directory", dir.string()},
|
|
{"command", std::string("clang++ -std=c++20 -c ") + file.string()},
|
|
{"file", file.string()},
|
|
});
|
|
}
|
|
writeFile(dir / "compile_commands.json", db.dump());
|
|
}
|
|
|
|
} // namespace
|
|
|
|
TEST_CASE("incremental fingerprints are deterministic and include transitive facts",
|
|
"[scan][incremental][fingerprint]") {
|
|
TempDir dir;
|
|
|
|
const auto source = dir.path / "fingerprint.cpp";
|
|
writeFile(source, "int global_value;\n"
|
|
"int leaf(int x){ global_value = x; return global_value; }\n"
|
|
"int caller(int x){ return leaf(x); }\n");
|
|
writeCompileDb(dir.path, std::vector<std::filesystem::path>{source});
|
|
|
|
std::string err;
|
|
auto db = fold::loadCompilationDatabase(dir.path.string(), err);
|
|
REQUIRE(db);
|
|
|
|
const auto first = fold::cli::collectFingerprints(*db, {source.string()});
|
|
const auto second = fold::cli::collectFingerprints(*db, {source.string()});
|
|
|
|
const auto* leaf = findFingerprintByName(first, "leaf");
|
|
const auto* caller = findFingerprintByName(first, "caller");
|
|
const auto* second_leaf = findFingerprintByName(second, "leaf");
|
|
const auto* second_caller = findFingerprintByName(second, "caller");
|
|
REQUIRE(leaf != nullptr);
|
|
REQUIRE(caller != nullptr);
|
|
REQUIRE(second_leaf != nullptr);
|
|
REQUIRE(second_caller != nullptr);
|
|
|
|
CHECK_FALSE(leaf->fingerprint.id.empty());
|
|
CHECK(leaf->fingerprint.source_hash != 0);
|
|
CHECK(caller->fingerprint.callee_hash != 0);
|
|
CHECK(caller->fingerprint.combined_hash ==
|
|
(caller->fingerprint.source_hash ^ caller->fingerprint.callee_hash));
|
|
CHECK(caller->summary.callees.contains(leaf->fingerprint.id));
|
|
CHECK(!leaf->summary.reads_globals.empty());
|
|
CHECK(!leaf->summary.writes_globals.empty());
|
|
CHECK(caller->summary.transitive_hash != 0);
|
|
|
|
CHECK(second_leaf->fingerprint.combined_hash == leaf->fingerprint.combined_hash);
|
|
CHECK(second_leaf->summary.transitive_hash == leaf->summary.transitive_hash);
|
|
CHECK(second_caller->fingerprint.combined_hash == caller->fingerprint.combined_hash);
|
|
CHECK(second_caller->summary.transitive_hash == caller->summary.transitive_hash);
|
|
}
|
|
|
|
TEST_CASE("incremental fingerprints preserve overloaded semantic identities",
|
|
"[scan][incremental][fingerprint][overload]") {
|
|
TempDir dir;
|
|
const auto source = dir.path / "overload_fingerprint.cpp";
|
|
writeFile(source, "int overloaded(int x){ return x; }\n"
|
|
"long overloaded(long x){ return x; }\n");
|
|
writeCompileDb(dir.path, std::vector<std::filesystem::path>{source});
|
|
|
|
std::string error;
|
|
auto database = fold::loadCompilationDatabase(dir.path.string(), error);
|
|
REQUIRE(database);
|
|
const auto fingerprints = fold::cli::collectFingerprints(*database, {source.string()});
|
|
|
|
std::vector<std::string> overload_ids;
|
|
for (const auto& [function_id, state] : fingerprints) {
|
|
if (state.fingerprint.qualified_name == "overloaded") {
|
|
CHECK(function_id == state.fingerprint.id);
|
|
overload_ids.push_back(function_id);
|
|
}
|
|
}
|
|
REQUIRE(overload_ids.size() == 2);
|
|
CHECK(overload_ids[0] != overload_ids[1]);
|
|
}
|
|
|
|
TEST_CASE("incremental identities separate internal functions from shared headers",
|
|
"[scan][incremental][fingerprint][internal-header]") {
|
|
TempDir dir;
|
|
const auto header = dir.path / "shared_internal.hpp";
|
|
const auto first_source = dir.path / "first_internal.cpp";
|
|
const auto second_source = dir.path / "second_internal.cpp";
|
|
writeFile(header, "static int header_helper(){ return 1; }\n");
|
|
writeFile(first_source,
|
|
"#include \"shared_internal.hpp\"\nint first_entry(){ return header_helper(); }\n");
|
|
writeFile(second_source,
|
|
"#include \"shared_internal.hpp\"\nint second_entry(){ return header_helper(); }\n");
|
|
writeCompileDb(dir.path, first_source, second_source);
|
|
|
|
std::string error;
|
|
auto database = fold::loadCompilationDatabase(dir.path.string(), error);
|
|
REQUIRE(database);
|
|
const auto fingerprints =
|
|
fold::cli::collectFingerprints(*database, {first_source.string(), second_source.string()});
|
|
|
|
std::vector<std::string> helper_ids;
|
|
for (const auto& [function_id, state] : fingerprints) {
|
|
if (state.fingerprint.qualified_name == "header_helper")
|
|
helper_ids.push_back(function_id);
|
|
}
|
|
REQUIRE(helper_ids.size() == 2);
|
|
CHECK(helper_ids[0] != helper_ids[1]);
|
|
}
|
|
|
|
TEST_CASE("incremental identities connect cross-TU declarations and definitions",
|
|
"[scan][incremental][fingerprint][cross-tu]") {
|
|
TempDir dir;
|
|
const auto caller_source = dir.path / "fingerprint_caller.cpp";
|
|
const auto callee_source = dir.path / "fingerprint_callee.cpp";
|
|
writeFile(
|
|
caller_source,
|
|
"int cross_tu_callee(int); int cross_tu_caller(int x){ return cross_tu_callee(x); }\n");
|
|
writeFile(callee_source, "int cross_tu_callee(int x){ return x + 1; }\n");
|
|
writeCompileDb(dir.path, caller_source, callee_source);
|
|
|
|
std::string error;
|
|
auto database = fold::loadCompilationDatabase(dir.path.string(), error);
|
|
REQUIRE(database);
|
|
const auto first =
|
|
fold::cli::collectFingerprints(*database, {caller_source.string(), callee_source.string()});
|
|
const auto* first_caller = findFingerprintByName(first, "cross_tu_caller");
|
|
const auto* first_callee = findFingerprintByName(first, "cross_tu_callee");
|
|
REQUIRE(first_caller != nullptr);
|
|
REQUIRE(first_callee != nullptr);
|
|
CHECK(first_caller->summary.callees.contains(first_callee->fingerprint.id));
|
|
|
|
fold::ir::ProjectCallGraph graph;
|
|
fold::cli::buildProjectCallGraph(*database, {caller_source.string(), callee_source.string()},
|
|
graph);
|
|
const auto graph_callers = graph.getCallerIds(first_callee->fingerprint.id);
|
|
CHECK(std::find(graph_callers.begin(), graph_callers.end(), first_caller->fingerprint.id) !=
|
|
graph_callers.end());
|
|
|
|
fold::detect::IncrementalCache cold;
|
|
for (const auto& [_, state] : first)
|
|
cold.insert(state);
|
|
cold.save((dir.path / "cache").string());
|
|
|
|
writeFile(callee_source, "int cross_tu_callee(int x){ return x + 2; }\n");
|
|
const auto second =
|
|
fold::cli::collectFingerprints(*database, {caller_source.string(), callee_source.string()});
|
|
const auto* second_caller = findFingerprintByName(second, "cross_tu_caller");
|
|
const auto* second_callee = findFingerprintByName(second, "cross_tu_callee");
|
|
REQUIRE(second_caller != nullptr);
|
|
REQUIRE(second_callee != nullptr);
|
|
|
|
auto warm = fold::detect::IncrementalCache::load((dir.path / "cache").string());
|
|
for (const auto& [_, state] : second)
|
|
warm.insert(state);
|
|
CHECK(warm.isStale(second_callee->fingerprint.id));
|
|
CHECK(warm.isStale(second_caller->fingerprint.id));
|
|
}
|
|
|
|
TEST_CASE("incremental global dependencies use cross-TU semantic identity",
|
|
"[scan][incremental][fingerprint][global]") {
|
|
TempDir dir;
|
|
const auto reader_source = dir.path / "global_reader.cpp";
|
|
const auto writer_source = dir.path / "global_writer.cpp";
|
|
writeFile(reader_source,
|
|
"extern int shared_global; int global_reader(){ return shared_global; }\n");
|
|
writeFile(writer_source, "int shared_global; void global_writer(){ shared_global = 2; }\n");
|
|
writeCompileDb(dir.path, reader_source, writer_source);
|
|
|
|
std::string error;
|
|
auto database = fold::loadCompilationDatabase(dir.path.string(), error);
|
|
REQUIRE(database);
|
|
const auto first =
|
|
fold::cli::collectFingerprints(*database, {reader_source.string(), writer_source.string()});
|
|
const auto* first_reader = findFingerprintByName(first, "global_reader");
|
|
const auto* first_writer = findFingerprintByName(first, "global_writer");
|
|
REQUIRE(first_reader != nullptr);
|
|
REQUIRE(first_writer != nullptr);
|
|
REQUIRE(first_reader->summary.reads_globals.size() == 1);
|
|
CHECK(first_writer->summary.writes_globals.contains(
|
|
*first_reader->summary.reads_globals.begin()));
|
|
|
|
fold::detect::IncrementalCache cold;
|
|
for (const auto& [_, state] : first)
|
|
cold.insert(state);
|
|
cold.save((dir.path / "cache").string());
|
|
|
|
writeFile(writer_source, "int shared_global; void global_writer(){ shared_global = 3; }\n");
|
|
const auto second =
|
|
fold::cli::collectFingerprints(*database, {reader_source.string(), writer_source.string()});
|
|
const auto* second_reader = findFingerprintByName(second, "global_reader");
|
|
REQUIRE(second_reader != nullptr);
|
|
auto warm = fold::detect::IncrementalCache::load((dir.path / "cache").string());
|
|
for (const auto& [_, state] : second)
|
|
warm.insert(state);
|
|
CHECK(warm.isStale(second_reader->fingerprint.id));
|
|
}
|
|
|
|
TEST_CASE("incremental cache ignores mtime-only changes",
|
|
"[scan][incremental][fingerprint][mtime]") {
|
|
TempDir dir;
|
|
fold::detect::IncrementalFunctionState state;
|
|
state.fingerprint.id = "usr:stable";
|
|
state.fingerprint.file = "stable.cpp";
|
|
state.fingerprint.qualified_name = "stable";
|
|
state.fingerprint.source_hash = 11;
|
|
state.fingerprint.callee_hash = 13;
|
|
state.fingerprint.combined_hash = 6;
|
|
state.fingerprint.last_modified = 17;
|
|
|
|
fold::detect::IncrementalCache cold;
|
|
cold.insert(state);
|
|
cold.save(dir.path.string());
|
|
|
|
auto warm = fold::detect::IncrementalCache::load(dir.path.string());
|
|
state.fingerprint.last_modified = 19;
|
|
warm.insert(state);
|
|
CHECK_FALSE(warm.isStale(state.fingerprint.id));
|
|
}
|
|
|
|
TEST_CASE("incremental cache rejects malformed versioned state",
|
|
"[scan][incremental][fingerprint][corrupt]") {
|
|
TempDir dir;
|
|
const auto cache_dir = dir.path / "cache";
|
|
std::filesystem::create_directories(cache_dir);
|
|
writeFile(cache_dir / "fingerprints.bin",
|
|
"v3\nusr:broken\tbroken.cpp\tbroken\tnot-a-number\t0\t0\t0\t\t\t\t\t0\n");
|
|
|
|
fold::detect::IncrementalFunctionState current;
|
|
current.fingerprint.id = "usr:current";
|
|
current.fingerprint.file = "current.cpp";
|
|
current.fingerprint.qualified_name = "current";
|
|
current.fingerprint.source_hash = 1;
|
|
auto cache = fold::detect::IncrementalCache::load(cache_dir.string());
|
|
cache.insert(current);
|
|
CHECK(cache.isStale(current.fingerprint.id));
|
|
|
|
writeFile(cache_dir / "fingerprints.bin", "v3\nusr:broken\tbroken.cpp\tbroken\t1\t0\t0\t0\t"
|
|
"18446744073709551616:\t\t\t\t0\n");
|
|
auto overflowed = fold::detect::IncrementalCache::load(cache_dir.string());
|
|
overflowed.insert(current);
|
|
CHECK(overflowed.isStale(current.fingerprint.id));
|
|
}
|
|
|
|
TEST_CASE("incremental input changes outside function bodies invalidate semantic work",
|
|
"[scan][incremental][fingerprint][macro]") {
|
|
TempDir dir;
|
|
const auto source = dir.path / "macro_change.cpp";
|
|
writeFile(source, "#define DIVISOR 1\nint macro_divide(){ return 10 / DIVISOR; }\n");
|
|
writeCompileDb(dir.path, std::vector<std::filesystem::path>{source});
|
|
|
|
std::string error;
|
|
auto database = fold::loadCompilationDatabase(dir.path.string(), error);
|
|
REQUIRE(database);
|
|
fold::cli::ScanOptions options;
|
|
options.detectors = {"div-by-zero"};
|
|
options.incremental = true;
|
|
options.cache_dir = (dir.path / "cache").string();
|
|
options.profile = true;
|
|
|
|
const auto cold = fold::cli::runDetectorsOnShard(*database, options, {source.string()}, true);
|
|
CHECK(cold.findings.empty());
|
|
writeFile(source, "#define DIVISOR 0\nint macro_divide(){ return 10 / DIVISOR; }\n");
|
|
const auto changed =
|
|
fold::cli::runDetectorsOnShard(*database, options, {source.string()}, true);
|
|
REQUIRE(changed.profile.has_value());
|
|
CHECK(changed.profile->project_index_input_mismatches == 1);
|
|
CHECK(changed.profile->functions_stale == 1);
|
|
CHECK_FALSE(changed.findings.empty());
|
|
}
|
|
|
|
TEST_CASE("dead-statement honors incremental function filtering",
|
|
"[scan][incremental][dead-statement]") {
|
|
TempDir dir;
|
|
const auto source = dir.path / "dead_statement_incremental.cpp";
|
|
writeFile(source,
|
|
"int stale_candidate(){ return 1; int unreachable = 2; return unreachable; }\n");
|
|
writeCompileDb(dir.path, std::vector<std::filesystem::path>{source});
|
|
|
|
std::string error;
|
|
auto database = fold::loadCompilationDatabase(dir.path.string(), error);
|
|
REQUIRE(database);
|
|
|
|
fold::cli::ScanOptions options;
|
|
options.detectors = {"dead-statement"};
|
|
options.incremental = true;
|
|
options.cache_dir = (dir.path / "cache").string();
|
|
options.profile = true;
|
|
|
|
const auto cold = fold::cli::runDetectorsOnShard(*database, options, {source.string()}, true);
|
|
REQUIRE_FALSE(cold.findings.empty());
|
|
const auto warm = fold::cli::runDetectorsOnShard(*database, options, {source.string()}, true);
|
|
CHECK(warm.findings.empty());
|
|
REQUIRE(warm.profile.has_value());
|
|
CHECK(warm.profile->functions_stale == 0);
|
|
}
|
|
|
|
TEST_CASE("scan_runner prepass supports cross-TU security lookup", "[scan][crosstu][security]") {
|
|
TempDir dir;
|
|
|
|
const auto caller = dir.path / "caller.cpp";
|
|
const auto callee = dir.path / "callee.cpp";
|
|
writeFile(caller,
|
|
"int sink_null(int* p); int sink_div(int x, int y); int sink_of(int a, int b);\n"
|
|
"int f(int x, int y, int a, int b){ int* p = nullptr; sink_null(p); sink_div(x, y); "
|
|
"return sink_of(a, b); }\n");
|
|
writeFile(callee, "int sink_null(int* p){ return *p; }\n"
|
|
"int sink_div(int x, int y){ return x / y; }\n"
|
|
"int sink_of(int a, int b){ return a + b; }\n");
|
|
writeCompileDb(dir.path, caller, callee);
|
|
|
|
std::string err;
|
|
auto db = fold::loadCompilationDatabase(dir.path.string(), err);
|
|
REQUIRE(db);
|
|
|
|
fold::cli::ScanOptions opts;
|
|
opts.compile_db = dir.path.string();
|
|
opts.detectors = {"null-deref", "div-by-zero", "int-overflow"};
|
|
|
|
auto result = fold::cli::runDetectorsOnShard(*db, opts, {caller.string(), callee.string()},
|
|
/*include_clone_clustering=*/false);
|
|
|
|
bool saw_null = false;
|
|
bool saw_div = false;
|
|
bool saw_overflow = false;
|
|
std::size_t cross_tu_calls = 0;
|
|
std::size_t explicit_preconditions = 0;
|
|
for (const auto& finding : result.findings) {
|
|
saw_null = saw_null || finding.kind == fold::FindingKind::NullDeref;
|
|
saw_div = saw_div || finding.kind == fold::FindingKind::DivByZero;
|
|
saw_overflow = saw_overflow || finding.kind == fold::FindingKind::IntOverflow;
|
|
if (finding.id.find(":call:") != std::string::npos) {
|
|
++cross_tu_calls;
|
|
CHECK(finding.evidence.is_summary_backed);
|
|
CHECK(finding.evidence.has_cross_tu_corroboration);
|
|
CHECK(finding.evidence.proof_mode != "exact");
|
|
if (finding.evidence.proof_mode == "precondition")
|
|
++explicit_preconditions;
|
|
}
|
|
}
|
|
|
|
REQUIRE(saw_null);
|
|
REQUIRE(saw_div);
|
|
REQUIRE(saw_overflow);
|
|
REQUIRE(cross_tu_calls >= 3);
|
|
REQUIRE(explicit_preconditions >= 2);
|
|
}
|
|
|
|
TEST_CASE("local security findings do not claim summary or cross-TU evidence",
|
|
"[scan][evidence][local]") {
|
|
TempDir dir;
|
|
const auto source = dir.path / "local.cpp";
|
|
writeFile(source, "int divide_local(int x){ int zero = 0; return x / zero; }\n"
|
|
"int overflow_local(int x){ return x + 1; }\n");
|
|
writeCompileDb(dir.path, std::vector<std::filesystem::path>{source});
|
|
|
|
std::string err;
|
|
auto db = fold::loadCompilationDatabase(dir.path.string(), err);
|
|
REQUIRE(db);
|
|
|
|
fold::cli::ScanOptions opts;
|
|
opts.compile_db = dir.path.string();
|
|
opts.detectors = {"div-by-zero", "int-overflow"};
|
|
auto result = fold::cli::runScanRouter(opts);
|
|
|
|
REQUIRE_FALSE(result.findings.empty());
|
|
for (const auto& finding : result.findings) {
|
|
CHECK_FALSE(finding.evidence.is_summary_backed);
|
|
CHECK_FALSE(finding.evidence.has_cross_tu_corroboration);
|
|
}
|
|
}
|
|
|
|
TEST_CASE("same-TU guarded division does not acquire a cached callsite warning",
|
|
"[scan][crosstu][guarded][same-tu]") {
|
|
TempDir dir;
|
|
const auto source = dir.path / "guarded.cpp";
|
|
writeFile(source, "int divide(int x, int denominator){ if(denominator == 0) return 0; "
|
|
"return x / denominator; }\n"
|
|
"int entry(){ return divide(42, 0); }\n");
|
|
writeCompileDb(dir.path, std::vector<std::filesystem::path>{source});
|
|
|
|
fold::cli::ScanOptions opts;
|
|
opts.compile_db = dir.path.string();
|
|
opts.detectors = {"div-by-zero"};
|
|
auto result = fold::cli::runScanRouter(opts);
|
|
|
|
REQUIRE(result.findings.empty());
|
|
}
|
|
|
|
TEST_CASE("symbolic summary priming reaches recursive SCC fixpoint", "[scan][summary][scc]") {
|
|
fold::detect::SummaryStore::instance().clear();
|
|
TempDir dir;
|
|
|
|
const auto caller = dir.path / "caller.cpp";
|
|
const auto callee = dir.path / "callee.cpp";
|
|
writeFile(caller, "int b(int);\n"
|
|
"int a(int x){ return b(x); }\n"
|
|
"int entry(int z){ if (z != 0) return a(z); return 0; }\n");
|
|
writeFile(callee, "int a(int);\n"
|
|
"int b(int x){ return 10 / x + a(x - 1); }\n");
|
|
writeCompileDb(dir.path, caller, callee);
|
|
|
|
std::string err;
|
|
auto db = fold::loadCompilationDatabase(dir.path.string(), err);
|
|
REQUIRE(db);
|
|
|
|
fold::cli::ScanOptions opts;
|
|
opts.compile_db = dir.path.string();
|
|
opts.detectors = {"div-by-zero"};
|
|
opts.build_summaries = "1";
|
|
|
|
auto result = fold::cli::runDetectorsOnShard(*db, opts, {caller.string(), callee.string()},
|
|
/*include_clone_clustering=*/false);
|
|
|
|
const auto* a_summary =
|
|
fold::detect::SummaryStore::instance().lookup(summaryKeyIn(caller, "a"));
|
|
const auto* b_summary =
|
|
fold::detect::SummaryStore::instance().lookup(summaryKeyIn(callee, "b"));
|
|
REQUIRE(b_summary != nullptr);
|
|
REQUIRE(a_summary != nullptr);
|
|
REQUIRE(!a_summary->param_pre.empty());
|
|
|
|
bool saw_recursive_callsite = false;
|
|
for (const auto& finding : result.findings) {
|
|
saw_recursive_callsite =
|
|
saw_recursive_callsite || (finding.kind == fold::FindingKind::DivByZero &&
|
|
finding.id.find("div-by-zero:call:") != std::string::npos &&
|
|
finding.message.find("`a`") != std::string::npos);
|
|
}
|
|
|
|
REQUIRE(saw_recursive_callsite);
|
|
}
|
|
|
|
TEST_CASE("symbolic summaries distinguish internal linkage functions by file",
|
|
"[scan][summary][crosstu]") {
|
|
fold::detect::SummaryStore::instance().clear();
|
|
TempDir dir;
|
|
|
|
const auto unsafe = dir.path / "unsafe.cpp";
|
|
const auto safe = dir.path / "safe.cpp";
|
|
writeFile(unsafe, "static int require_nonzero(int x){ return 10 / x; }\n"
|
|
"int unsafe_entry(int y){ return require_nonzero(y); }\n");
|
|
writeFile(safe, "static int require_nonzero(int x){ return x; }\n"
|
|
"int safe_entry(int y){ return require_nonzero(y); }\n");
|
|
writeCompileDb(dir.path, std::vector<std::filesystem::path>{unsafe, safe});
|
|
|
|
std::string err;
|
|
auto db = fold::loadCompilationDatabase(dir.path.string(), err);
|
|
REQUIRE(db);
|
|
|
|
fold::cli::ScanOptions opts;
|
|
opts.compile_db = dir.path.string();
|
|
opts.detectors = {"div-by-zero"};
|
|
opts.build_summaries = "1";
|
|
|
|
(void)fold::cli::runDetectorsOnShard(*db, opts, {unsafe.string(), safe.string()},
|
|
/*include_clone_clustering=*/false);
|
|
|
|
const auto* unsafe_summary =
|
|
fold::detect::SummaryStore::instance().lookup(summaryKeyIn(unsafe, "require_nonzero"));
|
|
const auto* safe_summary =
|
|
fold::detect::SummaryStore::instance().lookup(summaryKeyIn(safe, "require_nonzero"));
|
|
|
|
REQUIRE(unsafe_summary != nullptr);
|
|
REQUIRE(safe_summary != nullptr);
|
|
REQUIRE(!unsafe_summary->param_pre.empty());
|
|
REQUIRE(safe_summary->param_pre.empty());
|
|
}
|
|
|
|
TEST_CASE("Clang call graph adapter records constructor edges", "[scan][callgraph][clang]") {
|
|
TempDir dir;
|
|
|
|
const auto source = dir.path / "ctor.cpp";
|
|
writeFile(source, "int sink(int x){ return 10 / x; }\n"
|
|
"struct Wrapper { explicit Wrapper(int x){ sink(x); } };\n"
|
|
"int entry(int z){ Wrapper w(z); return z; }\n");
|
|
writeCompileDb(dir.path, std::vector<std::filesystem::path>{source});
|
|
|
|
std::string err;
|
|
auto db = fold::loadCompilationDatabase(dir.path.string(), err);
|
|
REQUIRE(db);
|
|
|
|
fold::ir::ProjectCallGraph graph;
|
|
fold::cli::primeCrossTUProperties(*db, {source.string()}, {"div-by-zero"}, &graph,
|
|
dir.path.string());
|
|
|
|
const auto entry_callees = graph.getCallees("entry");
|
|
bool saw_ctor = false;
|
|
std::string ctor_name;
|
|
for (const auto& callee : entry_callees) {
|
|
if (callee.find("Wrapper") != std::string::npos) {
|
|
saw_ctor = true;
|
|
ctor_name = callee;
|
|
}
|
|
}
|
|
REQUIRE(saw_ctor);
|
|
|
|
const auto ctor_callees = graph.getCallees(ctor_name);
|
|
bool saw_sink = false;
|
|
for (const auto& callee : ctor_callees)
|
|
saw_sink = saw_sink || callee == "sink";
|
|
REQUIRE(saw_sink);
|
|
}
|
|
|
|
TEST_CASE("source scans persist, reuse, invalidate, and fail closed on corrupt project graphs",
|
|
"[scan][callgraph][persistent]") {
|
|
TempDir dir;
|
|
const auto source = dir.path / "persistent.cpp";
|
|
writeFile(source, "int divide(int x){ return 10 / x; }\n"
|
|
"int entry(int x){ return divide(x); }\n");
|
|
writeCompileDb(dir.path, std::vector<std::filesystem::path>{source});
|
|
|
|
std::string error;
|
|
auto database = fold::loadCompilationDatabase(dir.path.string(), error);
|
|
REQUIRE(database);
|
|
|
|
fold::cli::ScanOptions options;
|
|
options.detectors = {"div-by-zero"};
|
|
options.incremental = true;
|
|
options.cache_dir = (dir.path / "cache").string();
|
|
options.profile = true;
|
|
|
|
const auto cold = fold::cli::runDetectorsOnShard(*database, options, {source.string()}, true);
|
|
REQUIRE(cold.profile.has_value());
|
|
CHECK(cold.profile->project_index_loaded_nodes == 0);
|
|
CHECK(cold.profile->project_index_changed_functions == 2);
|
|
|
|
const auto warm = fold::cli::runDetectorsOnShard(*database, options, {source.string()}, true);
|
|
REQUIRE(warm.profile.has_value());
|
|
CHECK(warm.profile->project_index_loaded_nodes >= 2);
|
|
CHECK(warm.profile->project_index_reused_functions == 2);
|
|
CHECK(warm.profile->project_index_changed_functions == 0);
|
|
|
|
writeFile(source, "int divide(int x){ return 20 / x; }\n"
|
|
"int entry(int x){ return divide(x); }\n");
|
|
const auto changed =
|
|
fold::cli::runDetectorsOnShard(*database, options, {source.string()}, true);
|
|
REQUIRE(changed.profile.has_value());
|
|
CHECK(changed.profile->project_index_changed_functions == 1);
|
|
CHECK(changed.profile->project_index_invalidated_dependents >= 1);
|
|
|
|
const auto index_path = dir.path / "cache" / "project_index.sqlite";
|
|
std::filesystem::remove(index_path);
|
|
writeFile(index_path, "corrupt");
|
|
const auto recovered =
|
|
fold::cli::runDetectorsOnShard(*database, options, {source.string()}, true);
|
|
REQUIRE(recovered.profile.has_value());
|
|
CHECK(recovered.profile->project_index_fallbacks == 1);
|
|
CHECK_FALSE(recovered.findings.empty());
|
|
const auto detector = std::find_if(
|
|
recovered.profile->detectors.begin(), recovered.profile->detectors.end(),
|
|
[](const fold::cli::DetectorProfile& profile) { return profile.name == "div-by-zero"; });
|
|
REQUIRE(detector != recovered.profile->detectors.end());
|
|
CHECK(detector->functions_analyzed == recovered.profile->num_functions);
|
|
CHECK(detector->functions_skipped == 0);
|
|
}
|
|
|
|
TEST_CASE("summary targeting follows every semantic overload path", "[scan][summary][callgraph]") {
|
|
TempDir dir;
|
|
const auto source = dir.path / "overloads.cpp";
|
|
writeFile(source, R"cpp(
|
|
int overloaded(int value) { return 10 / value; }
|
|
int overloaded(long value) { return 20 / static_cast<int>(value); }
|
|
int caller_a(int value) { return overloaded(value); }
|
|
int caller_b(long value) { return overloaded(value); }
|
|
)cpp");
|
|
writeCompileDb(dir.path, std::vector<std::filesystem::path>{source});
|
|
std::string error;
|
|
auto database = fold::loadCompilationDatabase(dir.path.string(), error);
|
|
REQUIRE(database);
|
|
|
|
fold::ir::ProjectCallGraph graph;
|
|
fold::cli::buildProjectCallGraph(*database, {source.string()}, graph);
|
|
auto& cache = fold::detect::CrossTUCache::instance();
|
|
cache.clear();
|
|
fold::detect::CalleeProperties properties;
|
|
properties.divisor_params.insert(0);
|
|
cache.insert("overloaded", properties);
|
|
fold::detect::SummaryStore::instance().clear();
|
|
|
|
const auto telemetry = fold::cli::primeSymbolicSummaries(*database, {"div-by-zero"}, graph);
|
|
CHECK(telemetry.target_functions == 3); // overloaded + both semantic callers
|
|
cache.clear();
|
|
fold::detect::SummaryStore::instance().clear();
|
|
}
|
|
|
|
TEST_CASE("project call graph resolves virtual and signature-compatible targets",
|
|
"[scan][callgraph][precision]") {
|
|
TempDir dir;
|
|
|
|
const auto source = dir.path / "dispatch.cpp";
|
|
writeFile(source, "struct Base { virtual int check(int x){ return x; } };\n"
|
|
"struct Derived : Base { int check(int x) override { return 10 / x; } };\n"
|
|
"int virtual_entry(Base& value, int x){ return value.check(x); }\n"
|
|
"int indirect_target(int x, int y){ return x / y; }\n"
|
|
"using BinaryFn = int (*)(int, int);\n"
|
|
"int indirect_entry(BinaryFn fn, int x, int y){ return fn(x, y); }\n"
|
|
"using UnknownFn = long (*)(char*);\n"
|
|
"long unknown_entry(UnknownFn fn, char* p){ return fn(p); }\n"
|
|
"int overloaded(int x){ return x; }\n"
|
|
"double overloaded(double x){ return x; }\n"
|
|
"int overload_entry(){ return overloaded(1); }\n");
|
|
writeCompileDb(dir.path, std::vector<std::filesystem::path>{source});
|
|
|
|
std::string error;
|
|
auto database = fold::loadCompilationDatabase(dir.path.string(), error);
|
|
REQUIRE(database);
|
|
|
|
fold::ir::ProjectCallGraph graph;
|
|
fold::cli::buildProjectCallGraph(*database, {source.string()}, graph);
|
|
|
|
const auto virtual_targets = graph.getCallees("virtual_entry");
|
|
CHECK(std::find(virtual_targets.begin(), virtual_targets.end(), "Base::check") !=
|
|
virtual_targets.end());
|
|
CHECK(std::find(virtual_targets.begin(), virtual_targets.end(), "Derived::check") !=
|
|
virtual_targets.end());
|
|
|
|
const auto indirect_targets = graph.getCallees("indirect_entry");
|
|
CHECK(std::find(indirect_targets.begin(), indirect_targets.end(), "indirect_target") !=
|
|
indirect_targets.end());
|
|
|
|
const auto unknown_ids = graph.getCalleeIds("unknown_entry");
|
|
REQUIRE(unknown_ids.size() == 1);
|
|
CHECK(graph.getNode(unknown_ids.front())->qualified_name == "<unknown-call>");
|
|
CHECK(graph.edgeCount(fold::ir::CallGraphEdgeKind::Unknown) == 1);
|
|
|
|
CHECK(graph.getNode("overloaded") == nullptr); // overload lookup is intentionally ambiguous
|
|
CHECK(graph.getCalleeIds("overload_entry").size() == 1);
|
|
}
|