fold/tests/unit/test_function_enumeration.cpp
trvon 4bc62d721a
fix: scan namespaced/nested functions; timeout robustness; null-deref drift
Enumeration: replace the hand-rolled top-level `tu->decls()` walks (TU
security prefilter + every interprocedural pre-pass/cross-TU primer in
div-by-zero/int-overflow/null-deref/oob) with a shared RAV-based
`fold::detect::forEachFunctionWithBody`, so functions nested in namespaces
and classes are analyzed. Fixes 0-findings on namespaced C++ (dbench CTF).

TU prefilter: extracted to fold/cli/tu_security_prefilter; flags unsigned
-/* wraps so int-overflow candidates in size arithmetic are not skipped.

Timeout robustness: cooperative per-TU walk deadline (WalkOptions) checked
in the walk/unrollLoop/CFG-block checkpoints and the BatchSafetyObserver
flush loop, plus a non-progress unroll fixpoint — slow TUs degrade with
partial findings instead of being SIGALRM-quarantined.

OOB precheck: treat `*(p+i)` and indirect callees as candidates.

null-deref drift fix: remove the unsound `priorFunctionTextProvesNonNull`
source-text heuristic (added in 331a68467) that treated a textual `p!=0`
before a deref as proof of non-null, silently suppressing real null-derefs
(CWE-476 F1 0.485 -> 0.583, recall 0.379 -> 0.517).

Tests: test_function_enumeration, test_tu_security_prefilter, namespaced
cross-TU int-overflow regression. Full suite green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 18:04:15 -04:00

80 lines
2.7 KiB
C++

#include <clang/AST/Decl.h>
#include <clang/Frontend/ASTUnit.h>
#include <clang/Tooling/Tooling.h>
#include <catch2/catch_test_macros.hpp>
#include <memory>
#include <set>
#include <string>
#include "fold/detect/function_enumeration.hpp"
namespace {
std::unique_ptr<clang::ASTUnit> parse(const std::string& code) {
return clang::tooling::buildASTFromCodeWithArgs(code, {"-std=c++20"}, "input.cpp");
}
// Collects qualified names of every function definition the helper visits.
std::multiset<std::string> visitedNames(const std::string& code) {
auto unit = parse(code);
REQUIRE(unit);
std::multiset<std::string> names;
fold::detect::forEachFunctionWithBody(unit->getASTContext(), [&](clang::FunctionDecl* fd) {
names.insert(fd->getQualifiedNameAsString());
});
return names;
}
} // namespace
TEST_CASE("forEachFunctionWithBody: visits free, namespaced, and class-method defs",
"[enumeration]") {
const std::string code = R"cpp(
void freeFn() {}
namespace ns {
void nsFree() {}
struct B {
void inlineMethod() {} // in-class inline, inside namespace
void outOfLine();
};
void B::outOfLine() {} // out-of-line def, inside namespace
}
)cpp";
auto names = visitedNames(code);
REQUIRE(names.count("freeFn") == 1);
REQUIRE(names.count("ns::nsFree") == 1);
REQUIRE(names.count("ns::B::inlineMethod") == 1);
REQUIRE(names.count("ns::B::outOfLine") == 1);
}
TEST_CASE("forEachFunctionWithBody: visits instantiated function templates", "[enumeration]") {
// A template is only a definition once instantiated; the call site forces
// instantiation. The helper should see at least the primary pattern.
const std::string code = R"cpp(
namespace ns {
template <typename T> T addOne(T x) { return x + 1; }
int use() { return addOne<int>(41); }
}
)cpp";
auto names = visitedNames(code);
REQUIRE(names.count("ns::addOne") >= 1);
REQUIRE(names.count("ns::use") == 1);
}
TEST_CASE("forEachFunctionWithBody: does not double-count a decl+def pair", "[enumeration]") {
const std::string code = R"cpp(
int declared(); // forward declaration
int declared() { return 0; } // definition
)cpp";
auto names = visitedNames(code);
REQUIRE(names.count("declared") == 1);
}
TEST_CASE("forEachFunctionWithBody: skips pure declarations with no body", "[enumeration]") {
const std::string code = R"cpp(
namespace ns { void onlyDeclared(); }
)cpp";
auto names = visitedNames(code);
REQUIRE(names.count("ns::onlyDeclared") == 0);
}