fold/tests/unit/test_loop_summary.cpp
trvon 080bf9655f
feat(walker): affine (symbolic-bound) loop summarization + deterministic baseline
Extend ir::Loop classification: a monotone single-induction-variable for-loop
with a symbolic bound (e.g. `for (i=0; i<n; ++i)`) is now Affine (was General).
The walker summarizes Affine loops like Counted ones — bind the induction
variable to a fresh symbolic, assert its start bound plus the loop condition
(encoded to a Z3 bool) as path constraints, walk the body once — exposing
high-index OOB that bounded unroll misses. Findings are over-approximations
(LoopHavoc/heuristic), never proved.

Net effect on the gate: oob recall 0.531 -> 0.559, F1 0.637 -> 0.652 (precision
dips 0.796 -> 0.782) — a contract-sanctioned over-approximation tradeoff.

Also regenerate bench/fp-baseline.json deterministically from a clean run: the
prior committed oob numbers (0.574) were not reproducible (transient warm-state
artifact); fresh runs are now bit-identical, so the gate is reliable.

Tests: test_ir_loop (affine classification), test_loop_summary (symbolic-bound
OOB exposed, not proved). Suite green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 08:46:16 -04:00

65 lines
2.9 KiB
C++

// B2: counted-loop summarization. When a counted loop's trip count exceeds the
// unroll bound, the induction variable is bound to its full closed-form range
// and the body is walked once, so an in-body OOB at a high index is exposed
// (bounded unroll would only see i = 0,1,2 and miss it). Findings from the
// summarized walk are over-approximations and carry lower (heuristic)
// confidence, never Proved.
#include <clang/Frontend/ASTUnit.h>
#include <clang/Tooling/Tooling.h>
#include <algorithm>
#include <catch2/catch_test_macros.hpp>
#include <memory>
#include <string>
#include <vector>
#include "fold/detect/cross_tu_cache.hpp"
#include "fold/detect/oob_access.hpp"
#include "fold/report/finding.hpp"
#include "fold/symbolic/budget.hpp"
namespace {
std::vector<fold::Finding> run(const std::string& code) {
// Cross-TU cache is a singleton; clear it so a prior case's function
// summary (these cases all define `f`) cannot leak buffer bounds across
// cases the way separate `fold scan` invocations never would.
fold::detect::CrossTUCache::instance().clear();
auto unit = clang::tooling::buildASTFromCodeWithArgs(code, {"-std=c++20"}, "input.cpp");
REQUIRE(unit);
fold::SolverBudget b;
fold::SolverStats s;
return fold::detectOOBAccessInContext(unit->getASTContext(), b, s);
}
} // namespace
TEST_CASE("loop-summary: high-index OOB in a counted loop is found", "[loop][oob][summary]") {
// i reaches 99; a[i] is out of bounds for i >= 10. Bounded unroll (3) only
// sees a[0..2] and misses this; the summarized full range [0,99] exposes it.
auto findings = run("void f(){ int a[10]; for (int i = 0; i < 100; ++i) a[i] = i; }");
REQUIRE(findings.size() >= 1);
REQUIRE(findings[0].kind == fold::FindingKind::OOBAccess);
// Over-approximation: must NOT be reported as a proof.
REQUIRE(findings[0].confidence != fold::Confidence::Proved);
}
TEST_CASE("loop-summary: affine (symbolic-bound) loop exposes potential OOB",
"[loop][oob][summary]") {
// `i < n` with n a parameter: i can reach >= 10 when n > 10, so a[i] is a
// potential overflow. The affine summary binds i to [0, n) and exposes it
// (bounded unroll sees only i=0,1,2). Over-approximation -> not Proved.
auto findings = run("void f(int n){ int a[10]; for (int i = 0; i < n; ++i) a[i] = i; }");
REQUIRE(findings.size() >= 1);
REQUIRE(findings[0].kind == fold::FindingKind::OOBAccess);
REQUIRE(findings[0].confidence != fold::Confidence::Proved);
}
TEST_CASE("loop-summary: in-bounds counted loop is not flagged", "[loop][oob][summary]") {
// i in [0,99], a has 100 elements -> every access in bounds -> no finding.
auto findings = run("void f(){ int a[100]; for (int i = 0; i < 100; ++i) a[i] = i; }");
bool any_oob = std::any_of(findings.begin(), findings.end(), [](const fold::Finding& f) {
return f.kind == fold::FindingKind::OOBAccess;
});
REQUIRE_FALSE(any_oob);
}