fold/tests/integration/test_cross_tu_security.cpp
trvon 46aaac427a
fix(summary): key function summaries by USR, not qualified name
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).
2026-09-24 20:49:38 -06:00

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);
}