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).
141 lines
5.8 KiB
C++
141 lines
5.8 KiB
C++
#include <clang/AST/RecursiveASTVisitor.h>
|
|
#include <clang/Frontend/ASTUnit.h>
|
|
#include <clang/Tooling/Tooling.h>
|
|
|
|
#include <catch2/catch_test_macros.hpp>
|
|
#include <memory>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
#include "fold/detect/null_deref.hpp"
|
|
#include "fold/detect/summary_store.hpp"
|
|
#include "fold/report/finding.hpp"
|
|
#include "fold/symbolic/budget.hpp"
|
|
#include "fold/symbolic/encoder_callee_names.hpp"
|
|
#include "fold/symbolic/summary.hpp"
|
|
#include "fold/symbolic/summary_encoder.hpp"
|
|
|
|
// Function summaries were stored under the qualified name, which overloads
|
|
// share: `take(int*, int)` (dereferences p) and `take(int*, long)` (checks p)
|
|
// both mapped to "take", so whichever was summarized first answered for both.
|
|
// A caller of the null-checking overload then inherited a nonnull
|
|
// precondition it does not have, and vice versa. Summaries are now keyed by
|
|
// the declaration's USR.
|
|
|
|
namespace {
|
|
|
|
std::unique_ptr<clang::ASTUnit> parse(const std::string& code) {
|
|
return clang::tooling::buildASTFromCodeWithArgs(code, {"-std=c++20"}, "input.cpp");
|
|
}
|
|
|
|
std::vector<const clang::FunctionDecl*> definitionsNamed(clang::ASTUnit& unit,
|
|
const std::string& name) {
|
|
struct Finder : clang::RecursiveASTVisitor<Finder> {
|
|
std::string name;
|
|
std::vector<const clang::FunctionDecl*> found;
|
|
bool VisitFunctionDecl(clang::FunctionDecl* fd) {
|
|
if (fd->getNameAsString() == name && fd->isThisDeclarationADefinition())
|
|
found.push_back(fd);
|
|
return true;
|
|
}
|
|
} finder;
|
|
finder.name = name;
|
|
finder.TraverseDecl(unit.getASTContext().getTranslationUnitDecl());
|
|
return finder.found;
|
|
}
|
|
|
|
bool requiresNonNull(const fold::symbolic::SymbolicSummary* summary, unsigned index) {
|
|
if (summary == nullptr)
|
|
return false;
|
|
const auto it = summary->param_pre.find(index);
|
|
if (it == summary->param_pre.end())
|
|
return false;
|
|
for (const auto& term : it->second)
|
|
if (term.kind == "nonnull")
|
|
return true;
|
|
return false;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
TEST_CASE("overloads get separate summaries", "[summary-key]") {
|
|
fold::detect::SummaryStore::instance().clear();
|
|
auto unit = parse("int take(int* p, int) { return *p; }\n"
|
|
"int take(int* p, long) { return p ? *p : 0; }\n");
|
|
REQUIRE(unit);
|
|
const auto defs = definitionsNamed(*unit, "take");
|
|
REQUIRE(defs.size() == 2);
|
|
const auto& ctx = unit->getASTContext();
|
|
|
|
SECTION("dereferencing overload first") {
|
|
CHECK(requiresNonNull(fold::ensureSymbolicSummary(defs[0], ctx), 0));
|
|
CHECK_FALSE(requiresNonNull(fold::ensureSymbolicSummary(defs[1], ctx), 0));
|
|
}
|
|
SECTION("null-checking overload first") {
|
|
CHECK_FALSE(requiresNonNull(fold::ensureSymbolicSummary(defs[1], ctx), 0));
|
|
CHECK(requiresNonNull(fold::ensureSymbolicSummary(defs[0], ctx), 0));
|
|
}
|
|
fold::detect::SummaryStore::instance().clear();
|
|
}
|
|
|
|
TEST_CASE("summary keys: identity across redeclarations, not across names", "[summary-key]") {
|
|
auto unit = parse("int f(int* p);\n"
|
|
"int f(int* p) { return *p; }\n"
|
|
"namespace a { int g(int* p) { return *p; } }\n"
|
|
"namespace b { int g(int* p) { return p ? *p : 0; } }\n");
|
|
REQUIRE(unit);
|
|
struct Finder : clang::RecursiveASTVisitor<Finder> {
|
|
std::vector<const clang::FunctionDecl*> f_decls;
|
|
std::vector<const clang::FunctionDecl*> g_defs;
|
|
bool VisitFunctionDecl(clang::FunctionDecl* fd) {
|
|
if (fd->getNameAsString() == "f")
|
|
f_decls.push_back(fd);
|
|
if (fd->getNameAsString() == "g")
|
|
g_defs.push_back(fd);
|
|
return true;
|
|
}
|
|
} finder;
|
|
finder.TraverseDecl(unit->getASTContext().getTranslationUnitDecl());
|
|
REQUIRE(finder.f_decls.size() == 2);
|
|
REQUIRE(finder.g_defs.size() == 2);
|
|
CHECK(fold::summaryStoreKey(finder.f_decls[0]) == fold::summaryStoreKey(finder.f_decls[1]));
|
|
CHECK(fold::summaryStoreKey(finder.g_defs[0]) != fold::summaryStoreKey(finder.g_defs[1]));
|
|
CHECK_FALSE(fold::summaryStoreKey(finder.f_decls[0]).empty());
|
|
CHECK(fold::summaryStoreKey(nullptr).empty());
|
|
}
|
|
|
|
TEST_CASE("null-deref: a cross-TU call uses its own overload's summary",
|
|
"[summary-key][null-deref]") {
|
|
// TU 1 defines both overloads and summarizes them (the dereferencing one
|
|
// last). TU 2 sees only declarations, so its call resolves through the
|
|
// store alone, and passes null to the overload that checks.
|
|
const std::string defs = "int take(int* p, long) { return p ? *p : 0; }\n"
|
|
"int take(int* p, int) { return *p; }\n";
|
|
const std::string caller = "int take(int* p, long);\n"
|
|
"int take(int* p, int);\n"
|
|
"int caller() { int* q = nullptr; return take(q, 1L); }\n"
|
|
"int control() { int* q = nullptr; return take(q, 1); }\n";
|
|
fold::detect::SummaryStore::instance().clear();
|
|
auto defs_unit = parse(defs);
|
|
REQUIRE(defs_unit);
|
|
for (const auto* fd : definitionsNamed(*defs_unit, "take"))
|
|
(void)fold::ensureSymbolicSummary(fd, defs_unit->getASTContext());
|
|
|
|
auto caller_unit = parse(caller);
|
|
REQUIRE(caller_unit);
|
|
fold::SolverBudget b;
|
|
fold::SolverStats s;
|
|
const auto findings = fold::detectNullDerefInContext(caller_unit->getASTContext(), b, s);
|
|
fold::detect::SummaryStore::instance().clear();
|
|
std::size_t in_caller = 0;
|
|
std::size_t in_control = 0;
|
|
for (const auto& f : findings) {
|
|
if (!f.locations.empty() && f.locations.front().line == 3)
|
|
++in_caller;
|
|
if (!f.locations.empty() && f.locations.front().line == 4)
|
|
++in_control;
|
|
}
|
|
CHECK(in_caller == 0);
|
|
// Control: the dereferencing overload's precondition still applies.
|
|
CHECK(in_control == 1);
|
|
}
|