fold/tests/unit/test_encoder_callee_names.cpp
trvon 00082448f4
fix(encoder): a global C++ lookalike is not a C library function
Problem: isCLibraryFunction accepted any global-scope non-member function,
so a project's `void free(Obj*)` overload was modeled as libc free: calling
it twice was a double free, and reading after it a use-after-free.

Fix: a global function counts only when it is extern "C", in namespace std,
or declared in a system header. extern "C" also covers C translation units,
where external functions have C language linkage.

Test: test_library_callee_identity "[global]" (DoubleFree and UseAfterFree
now 0; were 1 each); test_encoder_callee_names flips the global C++ helper
to not-a-library-callee. Full suite: 140/140.
2026-09-24 21:34:16 -06:00

182 lines
6.4 KiB
C++

#include <clang/AST/ASTContext.h>
#include <clang/AST/Decl.h>
#include <clang/AST/Expr.h>
#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/symbolic/encoder_callee_names.hpp"
namespace {
std::unique_ptr<clang::ASTUnit> parse(const std::string& code) {
return clang::tooling::buildASTFromCodeWithArgs(code, {"-std=c++20"}, "input.cpp");
}
const clang::FunctionDecl* functionNamed(clang::ASTUnit& unit, const std::string& qname) {
const clang::FunctionDecl* found = nullptr;
struct Visitor : clang::RecursiveASTVisitor<Visitor> {
const std::string& target;
const clang::FunctionDecl*& out;
Visitor(const std::string& t, const clang::FunctionDecl*& o) : target(t), out(o) {}
bool VisitFunctionDecl(clang::FunctionDecl* fd) {
if (fd->hasBody() && fd->getQualifiedNameAsString() == target) {
out = fd;
return false;
}
return true;
}
} visitor(qname, found);
visitor.TraverseDecl(unit.getASTContext().getTranslationUnitDecl());
return found;
}
std::vector<const clang::CallExpr*> collectCalls(clang::ASTUnit& unit, const std::string& in_func) {
std::vector<const clang::CallExpr*> calls;
const clang::FunctionDecl* fd = functionNamed(unit, in_func);
if (fd == nullptr || fd->getBody() == nullptr)
return calls;
struct Visitor : clang::RecursiveASTVisitor<Visitor> {
std::vector<const clang::CallExpr*>& out;
explicit Visitor(std::vector<const clang::CallExpr*>& o) : out(o) {}
bool VisitCallExpr(clang::CallExpr* ce) {
out.push_back(ce);
return true;
}
} visitor(calls);
visitor.TraverseStmt(fd->getBody());
return calls;
}
} // namespace
TEST_CASE("calleeName returns the unqualified function name for a direct call",
"[encoder][callee_names]") {
auto unit = parse(R"cpp(
void target(int);
void caller() { target(1); }
)cpp");
REQUIRE(unit);
auto calls = collectCalls(*unit, "caller");
REQUIRE(calls.size() == 1);
REQUIRE(fold::calleeName(calls[0]) == "target");
}
TEST_CASE("calleeName strips namespaces from the unqualified form", "[encoder][callee_names]") {
auto unit = parse(R"cpp(
namespace ns { void inner(); }
void caller() { ns::inner(); }
)cpp");
REQUIRE(unit);
auto calls = collectCalls(*unit, "caller");
REQUIRE(calls.size() == 1);
REQUIRE(fold::calleeName(calls[0]) == "inner");
}
TEST_CASE("calleeName extracts the method name from a C++ member call", "[encoder][callee_names]") {
auto unit = parse(R"cpp(
struct S { void method() {} };
void caller() { S s; s.method(); }
)cpp");
REQUIRE(unit);
auto calls = collectCalls(*unit, "caller");
REQUIRE(!calls.empty());
bool found_method_call = false;
for (const auto* ce : calls) {
if (fold::calleeName(ce) == "method")
found_method_call = true;
}
REQUIRE(found_method_call);
}
TEST_CASE("calleeName returns empty for null", "[encoder][callee_names]") {
REQUIRE(fold::calleeName(nullptr).empty());
}
TEST_CASE("qualifiedFunctionName returns the fully-qualified C++ name",
"[encoder][callee_names][qualified]") {
auto unit = parse(R"cpp(
namespace outer { namespace inner { void target() {} } }
)cpp");
REQUIRE(unit);
const clang::FunctionDecl* fd = functionNamed(*unit, "outer::inner::target");
REQUIRE(fd);
// External linkage: no @file suffix expected.
const std::string name = fold::qualifiedFunctionName(fd);
REQUIRE(name == "outer::inner::target");
}
TEST_CASE("qualifiedFunctionName appends file suffix for internal-linkage statics",
"[encoder][callee_names][qualified][linkage]") {
auto unit = parse(R"cpp(
static int helper() { return 0; }
)cpp");
REQUIRE(unit);
const clang::FunctionDecl* fd = functionNamed(*unit, "helper");
REQUIRE(fd);
const std::string name = fold::qualifiedFunctionName(fd);
// Should be "helper@<path>" — the @-suffix is what disambiguates
// same-named statics across TUs.
REQUIRE(name.starts_with("helper@"));
REQUIRE(name.find("input.cpp") != std::string::npos);
}
TEST_CASE("qualifiedFunctionName returns empty for null", "[encoder][callee_names][qualified]") {
REQUIRE(fold::qualifiedFunctionName(nullptr).empty());
}
TEST_CASE("qualifiedCalleeName resolves through a direct CallExpr",
"[encoder][callee_names][qualified]") {
auto unit = parse(R"cpp(
namespace ns { void target() {} }
void caller() { ns::target(); }
)cpp");
REQUIRE(unit);
auto calls = collectCalls(*unit, "caller");
REQUIRE(calls.size() == 1);
REQUIRE(fold::qualifiedCalleeName(calls[0]) == "ns::target");
}
TEST_CASE("qualifiedCalleeName returns empty for null", "[encoder][callee_names][qualified]") {
REQUIRE(fold::qualifiedCalleeName(nullptr).empty());
}
TEST_CASE("isCLibraryCallee: only C library functions, never members or namespaced lookalikes",
"[encoder-callee-names][library]") {
auto unit = parse("extern \"C\" void free(void*);\n"
"namespace std { using ::free; }\n"
"namespace mine { void free(void*) {} }\n"
"struct Pool { void free(void*) {} };\n"
"void global_helper(void*) {}\n"
"void f(void* p, Pool& pool, void (*fp)(void*)) {\n"
" free(p);\n"
" std::free(p);\n"
" mine::free(p);\n"
" pool.free(p);\n"
" global_helper(p);\n"
" fp(p);\n"
"}\n");
REQUIRE(unit);
const auto calls = collectCalls(*unit, "f");
REQUIRE(calls.size() == 6);
CHECK(fold::isCLibraryCallee(calls[0])); // ::free (extern "C")
CHECK(fold::isCLibraryCallee(calls[1])); // std::free resolves to ::free
CHECK_FALSE(fold::isCLibraryCallee(calls[2])); // mine::free
CHECK_FALSE(fold::isCLibraryCallee(calls[3])); // Pool::free
CHECK_FALSE(fold::isCLibraryCallee(calls[4])); // global C++ function, not extern "C"
CHECK_FALSE(fold::isCLibraryCallee(calls[5])); // indirect call
CHECK_FALSE(fold::isCLibraryCallee(nullptr));
}