fold/tests/unit/test_symbolic_summary.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

360 lines
13 KiB
C++

#include <clang/Frontend/ASTUnit.h>
#include <clang/Tooling/Tooling.h>
#include <unistd.h>
#include <catch2/catch_test_macros.hpp>
#include <memory>
#include <string>
#include "fold/detect/cross_tu_cache.hpp"
#include "fold/detect/div_by_zero.hpp"
#include "fold/detect/null_deref.hpp"
#include "fold/detect/summary_store.hpp"
#include "fold/symbolic/budget.hpp"
#include "fold/symbolic/encoder_callee_names.hpp"
#include "fold/symbolic/summary_encoder.hpp"
namespace {
std::unique_ptr<clang::ASTUnit> parse(const std::string& code, const std::string& file) {
return clang::tooling::buildASTFromCodeWithArgs(code, {"-std=c++20"}, file);
}
const clang::FunctionDecl* findFunction(clang::ASTUnit& unit, const std::string& name) {
auto& ctx = unit.getASTContext();
for (auto* decl : ctx.getTranslationUnitDecl()->decls()) {
auto* fd = clang::dyn_cast<clang::FunctionDecl>(decl);
if (!fd || !fd->hasBody())
continue;
if (fd->getQualifiedNameAsString() == name)
return fd;
}
return nullptr;
}
void primeLeafSummary(clang::ASTUnit& unit, const std::string& name) {
auto& ctx = unit.getASTContext();
const auto* fd = findFunction(unit, name);
REQUIRE(fd != nullptr);
auto summary = fold::summarizeLeafFunctionSymbolically(fd, ctx);
REQUIRE(summary);
fold::detect::SummaryStore::instance().store(fold::summaryStoreKey(fd), *summary);
}
} // namespace
TEST_CASE("symbolic summary suppresses guarded transitive divisor", "[summary][div]") {
fold::detect::CrossTUCache::instance().clear();
fold::detect::SummaryStore::instance().clear();
auto callee = parse("int plus_one(int x){ return x + 1; }", "callee.cpp");
auto caller = parse("int plus_one(int); int f(long y){ int z = (int)y; if (z != -1) return 10 "
"/ plus_one(z); return 0; }",
"caller.cpp");
REQUIRE(callee);
REQUIRE(caller);
primeLeafSummary(*callee, "plus_one");
fold::SolverBudget budget;
fold::SolverStats stats;
auto findings = fold::detectDivByZeroInContext(caller->getASTContext(), budget, stats);
REQUIRE(findings.empty());
REQUIRE(stats.queries == 1);
REQUIRE(stats.unsat == 1);
}
TEST_CASE("symbolic summary preserves transitive divisor hazard", "[summary][div][sat]") {
fold::detect::CrossTUCache::instance().clear();
fold::detect::SummaryStore::instance().clear();
auto callee = parse("int plus_one(int x){ return x + 1; }", "callee.cpp");
auto caller = parse("int plus_one(int); int f(long y){ int z = (int)y; if (z == -1) return 10 "
"/ plus_one(z); return 0; }",
"caller.cpp");
REQUIRE(callee);
REQUIRE(caller);
primeLeafSummary(*callee, "plus_one");
fold::SolverBudget budget;
fold::SolverStats stats;
auto findings = fold::detectDivByZeroInContext(caller->getASTContext(), budget, stats);
REQUIRE(findings.size() == 1);
REQUIRE(findings[0].kind == fold::FindingKind::DivByZero);
REQUIRE(stats.queries == 1);
REQUIRE(stats.sat == 1);
}
TEST_CASE("symbolic summaries compose through standalone calls", "[summary][div][transitive]") {
fold::detect::CrossTUCache::instance().clear();
fold::detect::SummaryStore::instance().clear();
auto unit = parse(R"(
int plus_one_ref(int& x) { x = x + 1; return x; }
int bump(int x) { plus_one_ref(x); return x; }
int relay(int x) { return bump(x); }
int f(int z) {
if (z != -1)
return 10 / relay(z);
return 0;
}
)",
"transitive.cpp");
REQUIRE(unit);
auto& ctx = unit->getASTContext();
const auto* bump = findFunction(*unit, "bump");
const auto* relay = findFunction(*unit, "relay");
REQUIRE(bump != nullptr);
REQUIRE(relay != nullptr);
const auto* bump_summary = fold::ensureSymbolicSummary(bump, ctx);
REQUIRE(bump_summary != nullptr);
REQUIRE(bump_summary->return_post != "arg0");
const auto* relay_summary = fold::ensureSymbolicSummary(relay, ctx);
REQUIRE(relay_summary != nullptr);
REQUIRE(relay_summary->return_post == bump_summary->return_post);
}
TEST_CASE("repeated nested summary computation stays stable across a reused scratch context",
"[summary][scratch]") {
fold::detect::CrossTUCache::instance().clear();
fold::detect::SummaryStore::instance().clear();
auto unit = parse(R"(
int id_val(int x) { return x; }
int wrap(int x) { return id_val(x); }
int outer(int x) { return wrap(x); }
)",
"scratch_reuse.cpp");
REQUIRE(unit);
auto& ctx = unit->getASTContext();
const auto* wrap = findFunction(*unit, "wrap");
const auto* outer = findFunction(*unit, "outer");
REQUIRE(wrap != nullptr);
REQUIRE(outer != nullptr);
// Compute the same summaries twice: the second pass exercises summary
// recomputation from a cleared store. Copies are taken because
// SummaryStore::lookupOrCompute returns pointers into its internal map,
// which clear() invalidates.
const auto wrap_first = *fold::ensureSymbolicSummary(wrap, ctx);
const auto outer_first = *fold::ensureSymbolicSummary(outer, ctx);
REQUIRE(!wrap_first.return_post.empty());
REQUIRE(!outer_first.return_post.empty());
fold::detect::SummaryStore::instance().clear();
const auto* wrap_second = fold::ensureSymbolicSummary(wrap, ctx);
const auto* outer_second = fold::ensureSymbolicSummary(outer, ctx);
REQUIRE(wrap_second != nullptr);
REQUIRE(outer_second != nullptr);
REQUIRE(wrap_second->return_post == wrap_first.return_post);
REQUIRE(outer_second->return_post == outer_first.return_post);
REQUIRE(wrap_second->reads_globals == wrap_first.reads_globals);
REQUIRE(wrap_second->writes_globals == wrap_first.writes_globals);
for (const auto& [param, pre] : wrap_first.param_pre) {
const auto it = wrap_second->param_pre.find(param);
REQUIRE(it != wrap_second->param_pre.end());
REQUIRE(it->second.size() == pre.size());
for (std::size_t i = 0; i < pre.size(); ++i) {
REQUIRE(it->second[i].kind == pre[i].kind);
REQUIRE(it->second[i].expr == pre[i].expr);
}
}
}
TEST_CASE("symbolic summaries record divisor preconditions", "[summary][pre][div]") {
fold::detect::CrossTUCache::instance().clear();
fold::detect::SummaryStore::instance().clear();
auto unit = parse(R"(
int div10(int x) { return 10 / x; }
int relay(int x) { return div10(x); }
int guarded(int x) { if (x != 0) return div10(x); return 0; }
)",
"pre_div.cpp");
REQUIRE(unit);
auto& ctx = unit->getASTContext();
const auto* div10 = findFunction(*unit, "div10");
const auto* relay = findFunction(*unit, "relay");
const auto* guarded = findFunction(*unit, "guarded");
REQUIRE(div10 != nullptr);
REQUIRE(relay != nullptr);
REQUIRE(guarded != nullptr);
const auto* div_summary = fold::ensureSymbolicSummary(div10, ctx);
REQUIRE(div_summary != nullptr);
REQUIRE(div_summary->param_pre.count(0) == 1);
REQUIRE(div_summary->param_pre.at(0).front().kind == "nonzero");
const auto* relay_summary = fold::ensureSymbolicSummary(relay, ctx);
REQUIRE(relay_summary != nullptr);
REQUIRE(relay_summary->param_pre.count(0) == 1);
REQUIRE(relay_summary->param_pre.at(0).front().kind == "nonzero");
const auto* guarded_summary = fold::ensureSymbolicSummary(guarded, ctx);
if (guarded_summary != nullptr)
REQUIRE(guarded_summary->param_pre.empty());
}
TEST_CASE("symbolic summaries retain the fallthrough side of an early-return guard",
"[summary][pre][div][early-return]") {
fold::detect::CrossTUCache::instance().clear();
fold::detect::SummaryStore::instance().clear();
auto unit = parse(R"(
int div10(int x) { return 10 / x; }
int guarded(int x) { if (x == 0) return 0; return div10(x); }
)",
"early_return.cpp");
REQUIRE(unit);
auto& ctx = unit->getASTContext();
const auto* guarded = findFunction(*unit, "guarded");
REQUIRE(guarded != nullptr);
const auto* summary = fold::ensureSymbolicSummary(guarded, ctx);
REQUIRE(summary != nullptr);
REQUIRE(summary->param_pre.empty());
}
TEST_CASE("symbolic summaries record pointer preconditions", "[summary][pre][null]") {
fold::detect::CrossTUCache::instance().clear();
fold::detect::SummaryStore::instance().clear();
auto unit = parse(R"(
int load(int* p) { return *p; }
int relay(int* p) { return load(p); }
int guarded(int* p) { if (p) return load(p); return 0; }
)",
"pre_null.cpp");
REQUIRE(unit);
auto& ctx = unit->getASTContext();
const auto* load = findFunction(*unit, "load");
const auto* relay = findFunction(*unit, "relay");
const auto* guarded = findFunction(*unit, "guarded");
REQUIRE(load != nullptr);
REQUIRE(relay != nullptr);
REQUIRE(guarded != nullptr);
const auto* load_summary = fold::ensureSymbolicSummary(load, ctx);
REQUIRE(load_summary != nullptr);
REQUIRE(load_summary->param_pre.count(0) == 1);
REQUIRE(load_summary->param_pre.at(0).front().kind == "nonnull");
const auto* relay_summary = fold::ensureSymbolicSummary(relay, ctx);
REQUIRE(relay_summary != nullptr);
REQUIRE(relay_summary->param_pre.count(0) == 1);
REQUIRE(relay_summary->param_pre.at(0).front().kind == "nonnull");
const auto* guarded_summary = fold::ensureSymbolicSummary(guarded, ctx);
if (guarded_summary != nullptr)
REQUIRE(guarded_summary->param_pre.empty());
}
TEST_CASE("summary preconditions are checked at divisor callsites", "[summary][pre][div][call]") {
fold::detect::CrossTUCache::instance().clear();
fold::detect::SummaryStore::instance().clear();
auto callee = parse("int div10(int x){ return 10 / x; }", "callee.cpp");
auto guarded = parse("int div10(int); int f(int z){ if (z != 0) return div10(z); return 0; }",
"guarded.cpp");
auto unguarded = parse("int div10(int); int f(int z){ return div10(z); }", "unguarded.cpp");
REQUIRE(callee);
REQUIRE(guarded);
REQUIRE(unguarded);
primeLeafSummary(*callee, "div10");
fold::SolverBudget budget;
fold::SolverStats guarded_stats;
auto guarded_findings =
fold::detectDivByZeroInContext(guarded->getASTContext(), budget, guarded_stats);
REQUIRE(guarded_findings.empty());
REQUIRE(guarded_stats.queries == 1);
REQUIRE(guarded_stats.unsat == 1);
fold::SolverStats unguarded_stats;
auto unguarded_findings =
fold::detectDivByZeroInContext(unguarded->getASTContext(), budget, unguarded_stats);
REQUIRE(unguarded_findings.size() == 1);
REQUIRE(unguarded_findings[0].kind == fold::FindingKind::DivByZero);
REQUIRE(unguarded_stats.queries == 1);
REQUIRE(unguarded_stats.sat == 1);
}
TEST_CASE("summary pointer preconditions prove address-of locals nonnull",
"[summary][pre][null][call]") {
fold::detect::CrossTUCache::instance().clear();
fold::detect::SummaryStore::instance().clear();
auto callee = parse("int load(int* p){ return *p; }", "callee.cpp");
auto guarded = parse(R"(
int load(int*);
int f() {
int x = 1;
int* p = &x;
return load(p);
}
)",
"guarded.cpp");
auto unguarded = parse(R"(
int load(int*);
int f() {
int* p = nullptr;
return load(p);
}
)",
"unguarded.cpp");
REQUIRE(callee);
REQUIRE(guarded);
REQUIRE(unguarded);
primeLeafSummary(*callee, "load");
fold::SolverBudget budget;
fold::SolverStats guarded_stats;
auto guarded_findings =
fold::detectNullDerefInContext(guarded->getASTContext(), budget, guarded_stats);
REQUIRE(guarded_findings.empty());
REQUIRE(guarded_stats.queries == 0);
REQUIRE(guarded_stats.unsat == 0);
fold::SolverStats unguarded_stats;
auto unguarded_findings =
fold::detectNullDerefInContext(unguarded->getASTContext(), budget, unguarded_stats);
REQUIRE(unguarded_findings.size() == 1);
REQUIRE(unguarded_findings[0].kind == fold::FindingKind::NullDeref);
REQUIRE(unguarded_stats.queries == 1);
REQUIRE(unguarded_stats.sat == 1);
}
TEST_CASE("symbolic summaries preserve sysconf page-size non-zero return",
"[summary][div][sysconf]") {
fold::detect::CrossTUCache::instance().clear();
fold::detect::SummaryStore::instance().clear();
const std::string code = "extern \"C\" long sysconf(int);\n"
"long page_size() { return sysconf(" +
std::to_string(static_cast<int>(_SC_PAGESIZE)) +
"); }\n"
"long relay() { return page_size(); }\n"
"long f(long x) { return x / relay(); }\n";
auto unit = parse(code, "sysconf_summary.cpp");
REQUIRE(unit);
fold::SolverBudget budget;
fold::SolverStats stats;
auto findings = fold::detectDivByZeroInContext(unit->getASTContext(), budget, stats);
REQUIRE(findings.empty());
REQUIRE(stats.sat == 0);
}