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.
136 lines
5.9 KiB
C++
136 lines
5.9 KiB
C++
#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/cross_tu_cache.hpp"
|
|
#include "fold/detect/double_free.hpp"
|
|
#include "fold/detect/taint_track.hpp"
|
|
#include "fold/detect/use_after_free.hpp"
|
|
#include "fold/report/finding.hpp"
|
|
#include "fold/symbolic/budget.hpp"
|
|
#include "fold/symbolic/encoder_callee_names.hpp"
|
|
|
|
// Library models (malloc/free, taint sources and sinks) were selected by the
|
|
// callee's bare identifier, so a project function that merely shares a libc
|
|
// name was modeled as libc: `mine::free(p)` followed by `free(p)` was a Proved
|
|
// double-free. Models now require the resolved declaration to be a C library
|
|
// function (global scope, namespace std, or extern "C"; never a member).
|
|
|
|
namespace {
|
|
|
|
std::unique_ptr<clang::ASTUnit> parse(const std::string& code) {
|
|
return clang::tooling::buildASTFromCodeWithArgs(code, {"-std=c++20"}, "input.cpp");
|
|
}
|
|
|
|
std::size_t countKind(const std::vector<fold::Finding>& findings, fold::FindingKind kind) {
|
|
std::size_t n = 0;
|
|
for (const auto& f : findings)
|
|
if (f.kind == kind)
|
|
++n;
|
|
return n;
|
|
}
|
|
|
|
std::vector<fold::Finding> runDoubleFree(const std::string& code) {
|
|
auto unit = parse(code);
|
|
REQUIRE(unit);
|
|
fold::SolverBudget b;
|
|
fold::SolverStats s;
|
|
return fold::detectDoubleFreeInContext(unit->getASTContext(), b, s);
|
|
}
|
|
|
|
std::vector<fold::Finding> runUseAfterFree(const std::string& code) {
|
|
auto unit = parse(code);
|
|
REQUIRE(unit);
|
|
return fold::detectUseAfterFreeInContext(unit->getASTContext());
|
|
}
|
|
|
|
std::vector<fold::Finding> runTaint(const std::string& code) {
|
|
fold::detect::CrossTUCache::instance().clear();
|
|
auto unit = parse(code);
|
|
REQUIRE(unit);
|
|
fold::SolverBudget b;
|
|
fold::SolverStats s;
|
|
return fold::detectTaintTrackInContext(unit->getASTContext(), b, s);
|
|
}
|
|
|
|
const std::string kLibc = "extern \"C\" void* malloc(unsigned long);\n"
|
|
"extern \"C\" void free(void*);\n";
|
|
|
|
} // namespace
|
|
|
|
TEST_CASE("heap model: a namespaced function named free is not libc free",
|
|
"[library-callee][double-free]") {
|
|
const std::string code = kLibc + "namespace mine { inline void free(void*) {} }\n"
|
|
"int f() {\n"
|
|
" int* p = static_cast<int*>(malloc(sizeof(int)));\n"
|
|
" if (!p) return 0;\n"
|
|
" *p = 1;\n"
|
|
" mine::free(p);\n"
|
|
" int v = *p;\n"
|
|
" free(p);\n"
|
|
" return v;\n"
|
|
"}\n";
|
|
CHECK(countKind(runDoubleFree(code), fold::FindingKind::DoubleFree) == 0);
|
|
CHECK(countKind(runUseAfterFree(code), fold::FindingKind::UseAfterFree) == 0);
|
|
}
|
|
|
|
TEST_CASE("heap model: a member function named free is not libc free",
|
|
"[library-callee][double-free]") {
|
|
const std::string code = kLibc + "struct Pool { void free(void*) {} };\n"
|
|
"void f(Pool& pool) {\n"
|
|
" void* p = malloc(8);\n"
|
|
" if (!p) return;\n"
|
|
" pool.free(p);\n"
|
|
" free(p);\n"
|
|
"}\n";
|
|
CHECK(countKind(runDoubleFree(code), fold::FindingKind::DoubleFree) == 0);
|
|
}
|
|
|
|
TEST_CASE("heap model: libc free twice is still a double free", "[library-callee][double-free]") {
|
|
// Negative control: the fix must not drop the real model.
|
|
const std::string code = kLibc + "void f() {\n"
|
|
" void* p = malloc(8);\n"
|
|
" if (!p) return;\n"
|
|
" free(p);\n"
|
|
" free(p);\n"
|
|
"}\n";
|
|
CHECK(countKind(runDoubleFree(code), fold::FindingKind::DoubleFree) == 1);
|
|
}
|
|
|
|
TEST_CASE("taint: a member function named system is not a command sink",
|
|
"[library-callee][taint]") {
|
|
const std::string code = "extern \"C\" char* getenv(const char*);\n"
|
|
"struct Shell { int system(const char*) { return 0; } };\n"
|
|
"int f(Shell& sh) { return sh.system(getenv(\"CMD\")); }\n";
|
|
CHECK(countKind(runTaint(code), fold::FindingKind::TaintTrack) == 0);
|
|
}
|
|
|
|
TEST_CASE("taint: libc system with tainted input is still a sink", "[library-callee][taint]") {
|
|
const std::string code = "extern \"C\" char* getenv(const char*);\n"
|
|
"extern \"C\" int system(const char*);\n"
|
|
"int f() { return system(getenv(\"CMD\")); }\n";
|
|
CHECK(countKind(runTaint(code), fold::FindingKind::TaintTrack) == 1);
|
|
}
|
|
|
|
TEST_CASE("heap model: a global C++ function named free is not libc free",
|
|
"[library-callee][double-free][global]") {
|
|
// A global-scope C++ overload (no extern "C", not in a system header) is a
|
|
// project function that shares libc's name, not the library function.
|
|
const std::string code = kLibc + "struct Obj { int v; };\n"
|
|
"void free(Obj*) {}\n"
|
|
"int f() {\n"
|
|
" Obj* o = static_cast<Obj*>(malloc(sizeof(Obj)));\n"
|
|
" if (!o) return 0;\n"
|
|
" o->v = 1;\n"
|
|
" free(o);\n"
|
|
" int v = o->v;\n"
|
|
" free(o);\n"
|
|
" return v;\n"
|
|
"}\n";
|
|
CHECK(countKind(runDoubleFree(code), fold::FindingKind::DoubleFree) == 0);
|
|
CHECK(countKind(runUseAfterFree(code), fold::FindingKind::UseAfterFree) == 0);
|
|
}
|