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.
182 lines
6.4 KiB
C++
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));
|
|
}
|