fold/tests/unit/test_oob_access.cpp
trvon a27c3366d8
fix(symbolic): cache string size()/length() across assignments
std::string/std::string_view size()/length() was cached only in the member
cache, which assign() clears. So `char* p = malloc(s.size()+1); ...; p[s.size()]`
encoded the two size() calls as DIFFERENT symbols, letting the solver find a
spurious out-of-bounds (yams object_storage_adapter.cpp dup_cstr, plugin_host_services_shim).

Route zero-arg const size()/length() through the assignment-surviving
string-length cache (string_lengths_), so both calls resolve to one length
symbol: the final write lands exactly at the end (in-bounds), while a genuine
size()+1 write still fires.

Adds positive/negative oob tests; full suite 137/137.
2026-08-23 09:11:45 -06:00

574 lines
26 KiB
C++

#include <clang/Tooling/Tooling.h>
#include <catch2/catch_test_macros.hpp>
#include <memory>
#include <string>
#include <vector>
#include "fold/detect/oob_access.hpp"
#include "fold/report/finding.hpp"
#include "fold/symbolic/budget.hpp"
namespace {
struct TestResult {
std::vector<fold::Finding> findings;
fold::SolverStats stats;
};
TestResult run(const std::string& code) {
auto unit = clang::tooling::buildASTFromCodeWithArgs(code, {"-std=c++20"}, "input.cpp");
REQUIRE(unit);
fold::SolverBudget b;
fold::SolverStats s;
auto findings = fold::detectOOBAccessInContext(unit->getASTContext(), b, s);
return {std::move(findings), s};
}
} // namespace
TEST_CASE("oob-access: constant index 10 in `int a[4]` flagged Proved", "[oob][const]") {
auto [findings, stats] = run("int f(){ int a[4] = {0}; return a[10]; }");
REQUIRE_FALSE(findings.empty());
REQUIRE(findings[0].kind == fold::FindingKind::OOBAccess);
REQUIRE(findings[0].confidence == fold::Confidence::Proved);
REQUIRE(stats.queries == 1);
REQUIRE(stats.sat == 1);
}
TEST_CASE("oob-access: negative constant index is formatted safely", "[oob][const]") {
auto [findings, stats] = run("int f(){ int a[4] = {0}; return a[-1]; }");
REQUIRE_FALSE(findings.empty());
REQUIRE(findings[0].kind == fold::FindingKind::OOBAccess);
REQUIRE(findings[0].message.find("buffer 'a'") != std::string::npos);
REQUIRE(findings[0].message.find("-1") != std::string::npos);
REQUIRE(findings[0].message.find("capacity 4") != std::string::npos);
REQUIRE(stats.sat == 1);
}
TEST_CASE("oob-access: constant index 2 in `int a[4]` is in-bounds", "[oob][safe]") {
auto [findings, stats] = run("int f(){ int a[4] = {0}; return a[2]; }");
REQUIRE(findings.empty());
REQUIRE(stats.queries == 1);
REQUIRE(stats.unsat == 1);
}
TEST_CASE("oob-access: size()+1 buffer with size() index is in-bounds", "[oob][string-length]") {
// `dup_cstr` idiom: malloc(b.size()+1) then p[b.size()]='\0'. The two
// size() calls must resolve to the SAME length symbol (the intervening
// assignment to `p` clears member_cache_ but not the string-length cache),
// so the final write lands exactly at the end, not out of bounds. Uses a
// self-contained struct because the test harness does not resolve
// <string>; stringLengthObjectKey is class-agnostic (zero-arg const
// size/length).
auto [findings, stats] = run("extern \"C\" void* malloc(unsigned long);\n"
"struct Buf { unsigned long n; unsigned long size() const; };\n"
"char* dup(Buf b) {\n"
" char* p = (char*)malloc(b.size() + 1);\n"
" if (!p) return nullptr;\n"
" p[b.size()] = '\\0';\n"
" return p;\n"
"}\n");
REQUIRE(findings.empty());
}
TEST_CASE("oob-access: size()+1 buffer with size()+1 index still flagged", "[oob][string-length]") {
// Clean negative: p[b.size()+1] is one byte past the end of the
// b.size()+1 allocation, so the genuine OOB must still fire.
auto [findings, stats] = run("extern \"C\" void* malloc(unsigned long);\n"
"struct Buf { unsigned long n; unsigned long size() const; };\n"
"char* dup(Buf b) {\n"
" char* p = (char*)malloc(b.size() + 1);\n"
" if (!p) return nullptr;\n"
" p[b.size() + 1] = '\\0';\n"
" return p;\n"
"}\n");
REQUIRE_FALSE(findings.empty());
REQUIRE(findings[0].kind == fold::FindingKind::OOBAccess);
}
TEST_CASE("oob-access: symbolic index produces a Heuristic finding", "[oob][symbolic]") {
auto [findings, stats] = run("int f(int i){ int a[4] = {0}; return a[i]; }");
REQUIRE_FALSE(findings.empty());
REQUIRE(findings[0].confidence == fold::Confidence::Heuristic);
REQUIRE(stats.queries == 1);
REQUIRE(stats.sat == 1);
}
TEST_CASE("oob-access: symbolic index expression produces a Heuristic finding", "[oob][symbolic]") {
auto [findings, stats] = run("int f(int i){ int a[4] = {0}; return a[i + 1]; }");
REQUIRE_FALSE(findings.empty());
REQUIRE(findings[0].confidence == fold::Confidence::Heuristic);
REQUIRE(stats.queries == 1);
REQUIRE(stats.sat == 1);
}
TEST_CASE("oob-access: guarded symbolic index suppresses the finding", "[oob][guarded]") {
// Path predicate inside the then-arm constrains i to [0, 4); OOB
// predicate is UNSAT under that constraint.
auto [findings, stats] = run("int f(int i){"
" int a[4] = {0};"
" if (i >= 0 && i < 4) return a[i];"
" return 0;"
"}");
REQUIRE(findings.empty());
REQUIRE(stats.queries == 1);
REQUIRE(stats.unsat == 1);
}
TEST_CASE("oob-access: min-bounded index suppresses the finding", "[oob][min]") {
// `lvl = min(p, 3)` is provably in [0, 3], so `a[lvl]` into a 4-element
// array is in-bounds. A user-defined `min` keeps the test independent of
// `<algorithm>` resolution in the synthetic AST. The bound is proven either
// by the syntactic min-bound pre-check (abstract_unsat) or by the solver
// (unsat) if the encoder inlines the call.
auto [findings, stats] =
run("static int min(int a, int b){ return a < b ? a : b; }\n"
"int f(int p){ int a[4] = {0}; int lvl = min(p, 3); return a[lvl]; }");
REQUIRE(findings.empty());
REQUIRE(stats.abstract_unsat + stats.unsat >= 1);
}
TEST_CASE("oob-access: modulo-bounded index suppresses the finding", "[oob][mod]") {
// `i % 4` is provably in [0, 3]; indexing a 4-element array is in-bounds.
// (The encoder proves the modulo bound natively; the syntactic check is a
// redundant safety net for opaque-divisor cases.)
auto [findings, stats] = run("int f(int i){ int a[4] = {0}; return a[i % 4]; }");
REQUIRE(findings.empty());
}
TEST_CASE("oob-access: clamp-bounded index suppresses the finding", "[oob][clamp]") {
// `clamp(i, 0, 3)` is provably in [0, 3] (bounded by the hi argument).
auto [findings, stats] =
run("static int clamp(int v, int lo, int hi){ return v < lo ? lo : (v > hi ? hi : v); }\n"
"int f(int i){ int a[4] = {0}; int c = clamp(i, 0, 3); return a[c]; }");
REQUIRE(findings.empty());
REQUIRE(stats.abstract_unsat + stats.unsat >= 1);
}
TEST_CASE("oob-access: single-trip loop guard suppresses second unrolled iteration",
"[oob][loop][guard]") {
auto [findings, stats] = run("int f(){"
" int amaxv[8] = {0};"
" int sum = 0;"
" for (int j = 0; j < 1; ++j) {"
" sum += amaxv[8 * j];"
" sum += amaxv[8 * j + 4];"
" }"
" return sum;"
"}");
REQUIRE(findings.empty());
REQUIRE(stats.sat == 0);
}
TEST_CASE("oob-access: Curl_str max+1 terminator index is safe", "[oob][curl][safe]") {
auto [findings, stats] =
run("struct Curl_str { const char* str; unsigned long len; };\n"
"extern int curlx_str_quotedword(const char**, Curl_str*, unsigned long);\n"
"#define curlx_strlen(x) ((x)->len)\n"
"int f(const char* in){ const char* line = in; Curl_str date{};\n"
" if(curlx_str_quotedword(&line, &date, 17)) return 0;\n"
" char dbuf[18]; dbuf[curlx_strlen(&date)] = 0; return dbuf[0]; }");
REQUIRE(findings.empty());
REQUIRE(stats.sat == 0);
}
TEST_CASE("oob-access: Curl_str bound does not hide undersized buffers", "[oob][curl][tp]") {
auto [findings, stats] =
run("struct Curl_str { const char* str; unsigned long len; };\n"
"extern int curlx_str_quotedword(const char**, Curl_str*, unsigned long);\n"
"#define curlx_strlen(x) ((x)->len)\n"
"int f(const char* in){ const char* line = in; Curl_str date{};\n"
" if(curlx_str_quotedword(&line, &date, 17)) return 0;\n"
" char dbuf[17]; dbuf[curlx_strlen(&date)] = 0; return dbuf[0]; }");
REQUIRE_FALSE(findings.empty());
REQUIRE(stats.sat >= 1);
}
TEST_CASE("oob-access: heap allocation with symbolic index is checked", "[oob][heap]") {
auto [findings, stats] =
run("extern \"C\" void* malloc(unsigned long);\n"
"int f(int i){ int* p = (int*)malloc(4 * sizeof(int)); return p[i]; }");
REQUIRE_FALSE(findings.empty());
REQUIRE(findings[0].kind == fold::FindingKind::OOBAccess);
REQUIRE_FALSE(findings[0].message.empty());
REQUIRE(stats.queries >= 1);
REQUIRE(stats.sat >= 1);
}
TEST_CASE("oob-access: heap byte offset 63 in 64-byte buffer is in-bounds", "[oob][heap][offset]") {
auto [findings, stats] = run("extern \"C\" void* malloc(unsigned long);\n"
"int f(){ char* buf = (char*)malloc(64); return buf[63]; }");
REQUIRE(findings.empty());
REQUIRE(stats.queries >= 1);
REQUIRE(stats.unsat >= 1);
}
TEST_CASE("oob-access: heap byte offset 64 in 64-byte buffer is flagged", "[oob][heap][offset]") {
auto [findings, stats] = run("extern \"C\" void* malloc(unsigned long);\n"
"int f(){ char* buf = (char*)malloc(64); return buf[64]; }");
REQUIRE_FALSE(findings.empty());
REQUIRE(findings[0].kind == fold::FindingKind::OOBAccess);
REQUIRE_FALSE(findings[0].message.empty());
REQUIRE(stats.queries >= 1);
REQUIRE(stats.sat >= 1);
}
TEST_CASE("oob-access: heap byte offset 100 in 64-byte buffer is flagged", "[oob][heap][offset]") {
auto [findings, stats] = run("extern \"C\" void* malloc(unsigned long);\n"
"int f(){ char* buf = (char*)malloc(64); return buf[100]; }");
REQUIRE_FALSE(findings.empty());
REQUIRE(findings[0].kind == fold::FindingKind::OOBAccess);
REQUIRE_FALSE(findings[0].message.empty());
REQUIRE(stats.queries >= 1);
REQUIRE(stats.sat >= 1);
}
TEST_CASE("oob-access: heap finding attributes the correct allocation site", "[oob][heap][site]") {
auto [findings, stats] = run("extern \"C\" void* malloc(unsigned long);\n"
"int f(){\n"
" char* a = (char*)malloc(64);\n"
" char* b = (char*)malloc(64);\n"
" return b[100];\n"
"}");
REQUIRE_FALSE(findings.empty());
REQUIRE(findings[0].kind == fold::FindingKind::OOBAccess);
REQUIRE(findings[0].message.find("malloc at input.cpp:4") != std::string::npos);
REQUIRE(findings[0].locations.size() >= 2);
REQUIRE(findings[0].locations[1].line == 4);
REQUIRE(findings[0].locations[1].function == "f");
REQUIRE(stats.queries >= 1);
REQUIRE(stats.sat >= 1);
}
TEST_CASE("oob-access: derived heap pointer preserves byte offset", "[oob][heap][offset]") {
auto [findings, stats] =
run("extern \"C\" void* malloc(unsigned long);\n"
"int f(){ char* buf = (char*)malloc(64); int* p = (int*)(buf + 10); return p[14]; }");
REQUIRE_FALSE(findings.empty());
REQUIRE(findings[0].kind == fold::FindingKind::OOBAccess);
REQUIRE_FALSE(findings[0].message.empty());
REQUIRE(stats.queries >= 1);
REQUIRE(stats.sat >= 1);
}
TEST_CASE("oob-access: C++ new array is tracked as heap allocation", "[oob][heap][new]") {
auto [findings, stats] = run("int f(){ char* buf = new char[64]; return buf[64]; }");
REQUIRE_FALSE(findings.empty());
REQUIRE(findings[0].kind == fold::FindingKind::OOBAccess);
REQUIRE(stats.sat >= 1);
}
TEST_CASE("oob-access: C++ delete array marks tracked allocation freed", "[oob][uaf][new]") {
auto [findings, stats] =
run("int f(){ char* buf = new char[4]; delete[] buf; return buf[0]; }");
bool saw_uaf = false;
for (const auto& finding : findings) {
saw_uaf = saw_uaf || finding.message.find("Use-after-free") != std::string::npos;
}
REQUIRE(saw_uaf);
REQUIRE(stats.sat >= 1);
}
TEST_CASE("oob-access: use-after-free on tracked heap pointer is flagged", "[oob][uaf]") {
auto [findings, stats] =
run("extern \"C\" void* malloc(unsigned long);\n"
"extern \"C\" void free(void*);\n"
"int f(){ int* p = (int*)malloc(sizeof(int)); free(p); return p[0]; }");
REQUIRE_FALSE(findings.empty());
bool saw_uaf = false;
for (const auto& finding : findings) {
saw_uaf = saw_uaf || finding.message.find("Use-after-free") != std::string::npos;
}
REQUIRE(saw_uaf);
REQUIRE(stats.queries >= 1);
REQUIRE(stats.sat >= 1);
}
TEST_CASE("oob-access: loop-allocated regions are not reported as cross-iteration uaf",
"[oob][uaf][loop]") {
auto [findings, stats] = run("extern \"C\" void* malloc(unsigned long);\n"
"extern \"C\" void free(void*);\n"
"int f(){\n"
" int* p = nullptr;\n"
" for (int i = 0; i < 2; ++i) {\n"
" p = (int*)malloc(sizeof(int));\n"
" free(p);\n"
" }\n"
" return p[0];\n"
"}");
bool saw_uaf = false;
for (const auto& finding : findings) {
saw_uaf = saw_uaf || finding.message.find("Use-after-free") != std::string::npos;
}
REQUIRE_FALSE(saw_uaf);
}
TEST_CASE("oob-access: memcpy into undersized stack buffer is flagged", "[oob][stdlib][stack]") {
auto [findings, stats] =
run("extern \"C\" void* memcpy(void*, const void*, unsigned long);\n"
"int f(){ char src[128] = {0}; char dst[64] = {0}; memcpy(dst, src, 100); return 0; }");
REQUIRE_FALSE(findings.empty());
REQUIRE(findings[0].message.find("memcpy") != std::string::npos);
REQUIRE(stats.sat >= 1);
}
TEST_CASE("oob-access: oversized memcpy length does not drive byte modeling loop",
"[oob][stdlib][wide-constant]") {
auto [findings, stats] =
run("extern \"C\" void* memcpy(void*, const void*, unsigned long long);\n"
"int f(){ char src[1] = {0}; char dst[1] = {0}; "
"memcpy(dst, src, 18446744073709551615ULL); return 0; }");
REQUIRE_FALSE(findings.empty());
REQUIRE(findings[0].message.find("18446744073709551615") != std::string::npos);
REQUIRE(stats.sat >= 1);
}
TEST_CASE("oob-access: memcpy within stack buffer capacity is safe", "[oob][stdlib][stack]") {
auto [findings, stats] =
run("extern \"C\" void* memcpy(void*, const void*, unsigned long);\n"
"int f(){ char src[128] = {0}; char dst[64] = {0}; memcpy(dst, src, 32); return 0; }");
REQUIRE(findings.empty());
REQUIRE(stats.unsat >= 1);
}
TEST_CASE("oob-access: memcpy into undersized heap buffer is flagged", "[oob][stdlib][heap]") {
auto [findings, stats] = run("extern \"C\" void* malloc(unsigned long);\n"
"extern \"C\" void* memcpy(void*, const void*, unsigned long);\n"
"int f(){ char src[128] = {0}; char* dst = (char*)malloc(64); "
"memcpy(dst, src, 100); return 0; }");
REQUIRE_FALSE(findings.empty());
REQUIRE(findings[0].message.find("memcpy") != std::string::npos);
REQUIRE(stats.sat >= 1);
}
TEST_CASE("oob-access: memcpy through stack pointer alias is flagged", "[oob][stdlib][stack]") {
auto [findings, stats] =
run("extern \"C\" void* memcpy(void*, const void*, unsigned long);\n"
"int f(){ char src[128] = {0}; char buf[64] = {0}; char* dst; dst = buf; "
"memcpy(dst, src, 100); return 0; }");
REQUIRE_FALSE(findings.empty());
REQUIRE(findings[0].message.find("memcpy") != std::string::npos);
REQUIRE(stats.sat >= 1);
}
TEST_CASE("oob-access: memcpy through heap pointer alias is flagged", "[oob][stdlib][heap]") {
auto [findings, stats] = run("extern \"C\" void* malloc(unsigned long);\n"
"extern \"C\" void* memcpy(void*, const void*, unsigned long);\n"
"int f(){ char src[128] = {0}; char* buf = (char*)malloc(64); "
"char* dst; dst = buf; memcpy(dst, src, 100); return 0; }");
REQUIRE_FALSE(findings.empty());
REQUIRE(findings[0].message.find("memcpy") != std::string::npos);
REQUIRE(stats.sat >= 1);
}
TEST_CASE("oob-access: snprintf bound beyond destination size is flagged", "[oob][stdlib][heap]") {
auto [findings, stats] =
run("extern \"C\" void* malloc(unsigned long);\n"
"extern \"C\" int snprintf(char*, unsigned long, const char*, ...);\n"
"int f(){ char* dst = (char*)malloc(64); return snprintf(dst, 100, \"%s\", \"x\"); }");
REQUIRE_FALSE(findings.empty());
REQUIRE(findings[0].message.find("snprintf") != std::string::npos);
REQUIRE(stats.sat >= 1);
}
TEST_CASE("oob-access: strlen uses tracked char-array initializer length",
"[oob][stdlib][strlen]") {
auto [findings, stats] =
run("extern \"C\" unsigned long strlen(const char*);\n"
"extern \"C\" int snprintf(char*, unsigned long, const char*, ...);\n"
"int f(){ char src[16] = \"abcdefghij\"; char dst[4] = {0}; "
"return snprintf(dst, strlen(src) + 1, \"%s\", src); }");
REQUIRE_FALSE(findings.empty());
REQUIRE(findings[0].message.find("snprintf") != std::string::npos);
REQUIRE(stats.sat >= 1);
}
TEST_CASE("oob-access: memcpy propagates copied string length to strlen", "[oob][stdlib][strlen]") {
auto [findings, stats] =
run("extern \"C\" void* memcpy(void*, const void*, unsigned long);\n"
"extern \"C\" unsigned long strlen(const char*);\n"
"extern \"C\" int snprintf(char*, unsigned long, const char*, ...);\n"
"int f(){ char src[8] = \"abcdef\"; char tmp[8] = {0}; char dst[4] = {0}; "
"memcpy(tmp, src, 7); return snprintf(dst, strlen(tmp) + 1, \"%s\", tmp); }");
REQUIRE_FALSE(findings.empty());
REQUIRE(findings[0].message.find("snprintf") != std::string::npos);
REQUIRE(stats.sat >= 1);
}
TEST_CASE("oob-access: strncpy propagates terminated string length to strlen",
"[oob][stdlib][strlen]") {
auto [findings, stats] =
run("extern \"C\" char* strncpy(char*, const char*, unsigned long);\n"
"extern \"C\" unsigned long strlen(const char*);\n"
"extern \"C\" int snprintf(char*, unsigned long, const char*, ...);\n"
"int f(){ char src[8] = \"abcdef\"; char tmp[8] = {0}; char dst[4] = {0}; "
"strncpy(tmp, src, 7); return snprintf(dst, strlen(tmp) + 1, \"%s\", tmp); }");
REQUIRE_FALSE(findings.empty());
REQUIRE(findings[0].message.find("snprintf") != std::string::npos);
REQUIRE(stats.sat >= 1);
}
TEST_CASE("oob-access: strcpy into undersized stack buffer is flagged", "[oob][stdlib][stack]") {
auto [findings, stats] = run(
"extern \"C\" char* strcpy(char*, const char*);\n"
"int f(){ char buf[10] = {0}; strcpy(buf, \"this string is way too long\"); return 0; }");
REQUIRE_FALSE(findings.empty());
REQUIRE(findings[0].message.find("strcpy") != std::string::npos);
REQUIRE(stats.sat >= 1);
}
TEST_CASE("oob-access: strcpy with fitting literal is safe", "[oob][stdlib][stack]") {
auto [findings, stats] =
run("extern \"C\" char* strcpy(char*, const char*);\n"
"int f(){ char buf[100] = {0}; strcpy(buf, \"short\"); return 0; }");
REQUIRE(findings.empty());
REQUIRE(stats.unsat >= 1);
}
TEST_CASE("oob-access: sprintf percent-s literal into undersized stack buffer is flagged",
"[oob][stdlib][stack]") {
auto [findings, stats] = run("extern \"C\" int sprintf(char*, const char*, ...);\n"
"int f(){ char buf[10] = {0}; return sprintf(buf, \"%s\", \"this "
"string is way too long\"); }");
REQUIRE_FALSE(findings.empty());
REQUIRE(findings[0].message.find("sprintf") != std::string::npos);
REQUIRE(stats.sat >= 1);
}
TEST_CASE("oob: snprintf fixed-width hex fits exact stack buffer", "[oob][snprintf][format]") {
auto [findings, stats] =
run("extern \"C\" int snprintf(char*, unsigned long, const char*, ...);\n"
"void f(unsigned char b){ char buf[3]; snprintf(buf, sizeof(buf), \"%02x\", b); }");
REQUIRE(findings.empty());
REQUIRE(stats.abstract_unsat >= 1);
}
TEST_CASE("oob: destination-bounded snprintf truncation is not an overflow",
"[oob][snprintf][format]") {
auto [findings, stats] =
run("extern \"C\" int snprintf(char*, unsigned long, const char*, ...);\n"
"void f(unsigned char b){ char buf[2]; snprintf(buf, sizeof(buf), \"%02x\", b); }");
REQUIRE(findings.empty());
REQUIRE(stats.abstract_unsat >= 1);
}
TEST_CASE("oob: snprintf fixed-width decimal fits full int stack buffer",
"[oob][snprintf][format]") {
auto [findings, stats] =
run("extern \"C\" int snprintf(char*, unsigned long, const char*, ...);\n"
"void f(int n){ char buf[12]; snprintf(buf, sizeof(buf), \"%03d\", n); }");
REQUIRE(findings.empty());
REQUIRE(stats.abstract_unsat >= 1);
}
TEST_CASE("oob: destination bound prevents minimum-width decimal overflow",
"[oob][snprintf][format]") {
auto [findings, stats] =
run("extern \"C\" int snprintf(char*, unsigned long, const char*, ...);\n"
"void f(int n){ char buf[8]; snprintf(buf, sizeof(buf), \"%03d\", n); }");
REQUIRE(findings.empty());
REQUIRE(stats.abstract_unsat >= 1);
}
TEST_CASE("oob: snprintf bounded clamp decimal fits small stack buffer",
"[oob][snprintf][format]") {
auto [findings, stats] =
run("namespace std { double lround(double); template<class T> T clamp(T, T, T); }\n"
"extern \"C\" int snprintf(char*, unsigned long, const char*, ...);\n"
"struct C { double alpha; };\n"
"void f(C c){ int a100 = static_cast<int>(std::lround(std::clamp(c.alpha, 0.0, 1.0) * "
"100.0)); "
"char buf[8]; snprintf(buf, sizeof(buf), \"%03d\", a100); }");
REQUIRE(findings.empty());
REQUIRE(stats.abstract_unsat >= 1);
}
TEST_CASE("oob: uniform int distribution index within array length", "[oob][array][random]") {
auto [findings, stats] =
run("namespace std { template<class T=int> struct uniform_int_distribution { "
"uniform_int_distribution(int, int); int operator()(int&); }; } "
"char f(int& gen){ const char hex_chars[] = \"0123456789abcdef\"; "
"std::uniform_int_distribution<> dis(0, 15); "
"return hex_chars[dis(gen)]; }");
REQUIRE(findings.empty());
REQUIRE(stats.abstract_unsat + stats.unsat >= 1);
}
TEST_CASE("oob: readlink count bounded by destination minus nul", "[oob][array][readlink]") {
auto [findings, stats] = run(
"extern \"C\" long readlink(const char*, char*, unsigned long); "
"void f(const char* path){ char buf[4096]; long n = readlink(path, buf, sizeof(buf) - 1); "
"if(n > 0) buf[n] = '\\0'; }");
REQUIRE(findings.empty());
REQUIRE(stats.abstract_unsat + stats.unsat >= 1);
}
TEST_CASE("oob: snprintf destination-sized bound suppresses long float format",
"[oob][snprintf][format]") {
auto [findings, stats] =
run("extern \"C\" int snprintf(char*, unsigned long, const char*, ...); "
"void f(double value){ char buf[16]; snprintf(buf, sizeof(buf), \"%.3f\", value); }");
REQUIRE(findings.empty());
REQUIRE(stats.abstract_unsat + stats.unsat >= 1);
}
TEST_CASE("oob: interprocedural buffer-bound from decayed stack array", "[oob][interproc]") {
// T160: decode_base64(basic_auth, decoded) where decoded[256] decays
// to char* output. The callee writes output[output_index++] in an
// unbounded loop. Fold must thread the 256-element buffer size from
// the call site and flag the OOB.
auto [findings, stats] =
run("void decode_base64(const char* input, char* output);\n" // fwd decl
"int f(const char* basic_auth) {\n"
" char decoded[256];\n"
" decode_base64(basic_auth, decoded);\n"
" return decoded[0];\n"
"}\n"
"void decode_base64(const char* input, char* output) {\n"
" int output_index = 0;\n"
" for (int i = 0; input[i] != 0; ++i) {\n"
" output[output_index++] = input[i];\n"
" }\n"
" output[output_index] = 0;\n"
"}");
REQUIRE_FALSE(findings.empty());
bool saw_oob_on_output = false;
for (const auto& f : findings) {
if (f.kind == fold::FindingKind::OOBAccess && f.evidence.variable_name == "output") {
saw_oob_on_output = true;
}
}
REQUIRE(saw_oob_on_output);
}
TEST_CASE("oob: pre-pass seeds callee buffer from caller ConstantArrayType arg",
"[oob][interproc]") {
// T163: Callee defined BEFORE caller (declaration order). Caller
// passes a stack array (ConstantArrayType) to the callee. The callee
// has an ArraySubscriptExpr that writes past the buffer bounds.
// The pre-pass seedInterproceduralContext() must populate
// param_buffer_sizes_ before any function walk so the callee's
// OOB is flagged even though it appears first in the TU.
auto [findings, stats] = run("void fill_buf(char* buf) {\n" // callee FIRST, no fwd decl
" int i = 0;\n"
" while (i < 20) {\n"
" buf[i] = (char)('a' + i);\n"
" i++;\n"
" }\n"
" buf[i] = 0;\n"
"}\n"
"int caller() {\n" // caller SECOND
" char small[8];\n" // ConstantArrayType char[8]
" fill_buf(small);\n"
" return small[0];\n"
"}");
REQUIRE_FALSE(findings.empty());
bool saw_oob_on_buf = false;
for (const auto& f : findings) {
if (f.kind == fold::FindingKind::OOBAccess && f.evidence.variable_name == "buf") {
saw_oob_on_buf = true;
}
}
REQUIRE(saw_oob_on_buf);
}