diff --git a/README.md b/README.md index 02eb975..92f1705 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,7 @@ --format=json|sarif|human --quiet disables diagnostics entirely --warnings-only keeps only important diagnostics +--stack-limit= overrides stack limit (bytes, or KiB/MiB/GiB) --compile-arg= passes an extra argument to the compiler -I or -I adds an include directory -D[=value] or -D [=value] defines a macro diff --git a/main.cpp b/main.cpp index 43ef72d..8a0c346 100644 --- a/main.cpp +++ b/main.cpp @@ -1,10 +1,12 @@ #include "StackUsageAnalyzer.hpp" #include +#include #include #include // strncmp, strcmp #include #include +#include #include #include #include @@ -39,6 +41,7 @@ static void printHelp() << " --only-file= Only report functions from this source file\n" << " --only-dir= Only report functions under this directory\n" << " --only-func= Only report functions with this name (comma-separated)\n" + << " --stack-limit= Override stack size limit (bytes, or KiB/MiB/GiB)\n" << " --dump-filter Print filter decisions to stderr\n" << " --quiet Suppress per-function diagnostics\n" << " --warnings-only Show warnings and errors only\n" @@ -253,6 +256,87 @@ static std::string trimCopy(const std::string& input) return input.substr(start, end - start); } +static bool parseStackLimitValue(const std::string& input, StackSize& out, std::string& error) +{ + std::string trimmed = trimCopy(input); + if (trimmed.empty()) + { + error = "stack limit is empty"; + return false; + } + + std::size_t digitCount = 0; + while (digitCount < trimmed.size() && + std::isdigit(static_cast(trimmed[digitCount]))) + { + ++digitCount; + } + if (digitCount == 0) + { + error = "stack limit must start with a number"; + return false; + } + + const std::string numberPart = trimmed.substr(0, digitCount); + std::string suffix = trimCopy(trimmed.substr(digitCount)); + + unsigned long long base = 0; + auto [ptr, ec] = + std::from_chars(numberPart.data(), numberPart.data() + numberPart.size(), base, 10); + if (ec != std::errc() || ptr != numberPart.data() + numberPart.size()) + { + error = "invalid numeric value"; + return false; + } + if (base == 0) + { + error = "stack limit must be greater than zero"; + return false; + } + + StackSize multiplier = 1; + if (!suffix.empty()) + { + std::string lowered; + lowered.reserve(suffix.size()); + for (char c : suffix) + { + lowered.push_back(static_cast(std::tolower(static_cast(c)))); + } + + if (lowered == "b") + { + multiplier = 1; + } + else if (lowered == "k" || lowered == "kb" || lowered == "kib") + { + multiplier = 1024ull; + } + else if (lowered == "m" || lowered == "mb" || lowered == "mib") + { + multiplier = 1024ull * 1024ull; + } + else if (lowered == "g" || lowered == "gb" || lowered == "gib") + { + multiplier = 1024ull * 1024ull * 1024ull; + } + else + { + error = "unsupported suffix (use bytes, KiB, MiB, or GiB)"; + return false; + } + } + + if (base > std::numeric_limits::max() / multiplier) + { + error = "stack limit is too large"; + return false; + } + + out = static_cast(base) * multiplier; + return true; +} + static void addFunctionFilters(std::vector& dest, const std::string& input) { std::string current; @@ -421,6 +505,35 @@ int main(int argc, char** argv) cfg.onlyDirs.emplace_back(argv[++i]); continue; } + if (argStr == "--stack-limit") + { + if (i + 1 >= argc) + { + llvm::errs() << "Missing argument for --stack-limit\n"; + return 1; + } + std::string error; + StackSize value = 0; + if (!parseStackLimitValue(argv[++i], value, error)) + { + llvm::errs() << "Invalid --stack-limit value: " << error << "\n"; + return 1; + } + cfg.stackLimit = value; + continue; + } + if (argStr.rfind("--stack-limit=", 0) == 0) + { + std::string error; + StackSize value = 0; + if (!parseStackLimitValue(argStr.substr(std::strlen("--stack-limit=")), value, error)) + { + llvm::errs() << "Invalid --stack-limit value: " << error << "\n"; + return 1; + } + cfg.stackLimit = value; + continue; + } if (argStr == "--dump-filter") { cfg.dumpFilter = true; diff --git a/run_test.py b/run_test.py index 031a656..fb195f9 100755 --- a/run_test.py +++ b/run_test.py @@ -39,6 +39,7 @@ def extract_expectations(c_path: Path): """ expectations = [] negative_expectations = [] + stack_limit = None lines = c_path.read_text().splitlines() i = 0 n = len(lines) @@ -47,6 +48,12 @@ def extract_expectations(c_path: Path): raw = lines[i] stripped = raw.lstrip() + stack_match = re.match(r"//\s*stack-limit\s*[:=]\s*(\S+)", stripped, re.IGNORECASE) + if stack_match: + stack_limit = stack_match.group(1) + i += 1 + continue + stripped_line = stripped if stripped_line.startswith("// not contains:"): negative = stripped_line[len("// not contains:"):].strip() @@ -77,15 +84,18 @@ def extract_expectations(c_path: Path): else: i += 1 - return expectations, negative_expectations + return expectations, negative_expectations, stack_limit -def run_analyzer_on_file(c_path: Path) -> str: +def run_analyzer_on_file(c_path: Path, stack_limit=None) -> str: """ Lance ton analyseur sur un fichier C et récupère stdout+stderr. """ + args = [str(ANALYZER), str(c_path)] + if stack_limit: + args.append(f"--stack-limit={stack_limit}") result = subprocess.run( - [str(ANALYZER), str(c_path)], + args, capture_output=True, text=True, ) @@ -185,8 +195,12 @@ def parse_human_functions(output: str): functions[name]["isRecursive"] = True elif "unconditional self recursion detected" in stripped: functions[name]["hasInfiniteSelfRecursion"] = True - elif "potential stack overflow: exceeds limit of" in stripped: + + # Stack overflow diagnostics can appear after a location line. + for block_line in block[1:]: + if "potential stack overflow: exceeds limit of" in block_line: functions[name]["exceedsLimit"] = True + break i = j return functions @@ -712,12 +726,12 @@ def check_file(c_path: Path): dans la sortie de l'analyseur. """ print(f"=== Testing {c_path} ===") - expectations, negative_expectations = extract_expectations(c_path) + expectations, negative_expectations, stack_limit = extract_expectations(c_path) if not expectations and not negative_expectations: print(" (no expectations found, skipping)\n") return True, 0, 0 - analyzer_output = run_analyzer_on_file(c_path) + analyzer_output = run_analyzer_on_file(c_path, stack_limit=stack_limit) norm_output = normalize(analyzer_output) all_ok = True diff --git a/src/StackUsageAnalyzer.cpp b/src/StackUsageAnalyzer.cpp index 1e91b56..33eefb3 100644 --- a/src/StackUsageAnalyzer.cpp +++ b/src/StackUsageAnalyzer.cpp @@ -67,6 +67,7 @@ namespace ctrace::stack StackSize bytes = 0; bool unknown = false; bool hasDynamicAlloca = false; + std::vector> localAllocas; }; // État interne pour la propagation @@ -3003,6 +3004,8 @@ namespace ctrace::stack // Analyse locale de la stack (deux variantes) // ============================================================================ + static std::string deriveAllocaName(const llvm::AllocaInst* AI); + static LocalStackInfo computeLocalStackBase(llvm::Function& F, const llvm::DataLayout& DL) { LocalStackInfo info; @@ -3035,6 +3038,7 @@ namespace ctrace::stack StackSize size = DL.getTypeAllocSize(ty) * count; info.bytes += size; + info.localAllocas.emplace_back(deriveAllocaName(alloca), size); } } @@ -3685,6 +3689,87 @@ namespace ctrace::stack return {}; } + static bool getFunctionSourceLocation(const llvm::Function& F, unsigned& line, + unsigned& column) + { + line = 0; + column = 0; + + for (const llvm::BasicBlock& BB : F) + { + for (const llvm::Instruction& I : BB) + { + llvm::DebugLoc DL = I.getDebugLoc(); + if (!DL) + continue; + line = DL.getLine(); + column = DL.getCol(); + if (line != 0) + { + if (column == 0) + column = 1; + return true; + } + } + } + + if (auto* sp = F.getSubprogram()) + { + line = sp->getLine(); + if (line != 0) + { + column = 1; + return true; + } + } + + return false; + } + + static std::string buildMaxStackCallPath(const llvm::Function* F, const CallGraph& CG, + const InternalAnalysisState& state) + { + std::string path; + std::set visited; + const llvm::Function* current = F; + + while (current) + { + if (!visited.insert(current).second) + break; + + if (!path.empty()) + path += " -> "; + path += current->getName().str(); + + const llvm::Function* bestCallee = nullptr; + StackEstimate bestStack{}; + + auto itCG = CG.find(current); + if (itCG == CG.end()) + break; + + for (const llvm::Function* callee : itCG->second) + { + auto itTotal = state.TotalStack.find(callee); + StackEstimate est = + (itTotal != state.TotalStack.end()) ? itTotal->second : StackEstimate{}; + if (!bestCallee || est.bytes > bestStack.bytes) + { + bestCallee = callee; + bestStack = est; + } + } + + if (!bestCallee || bestStack.bytes == 0) + break; + + current = bestCallee; + } + + return path; + } + static std::string normalizePathForMatch(const std::string& input) { std::string out = input; @@ -3942,6 +4027,10 @@ namespace ctrace::stack // 1) Stack locale par fonction std::map LocalStack; + std::map> functionLocations; + std::map functionCallPaths; + std::map>> functionLocalAllocas; + for (llvm::Function& F : mod) { if (F.isDeclaration()) @@ -4051,6 +4140,23 @@ namespace ctrace::stack fr.hasInfiniteSelfRecursion = state.InfiniteRecursionFuncs.count(Fn) != 0; fr.exceedsLimit = (!fr.maxStackUnknown && totalInfo.bytes > config.stackLimit); + unsigned line = 0; + unsigned column = 0; + if (getFunctionSourceLocation(F, line, column)) + { + functionLocations[fr.name] = {line, column}; + } + if (!fr.isRecursive && totalInfo.bytes > localInfo.bytes) + { + std::string path = buildMaxStackCallPath(Fn, CG, state); + if (!path.empty()) + functionCallPaths[fr.name] = path; + } + if (!localInfo.localAllocas.empty()) + { + functionLocalAllocas[fr.name] = localInfo.localAllocas; + } + result.functions.push_back(std::move(fr)); } @@ -4087,8 +4193,88 @@ namespace ctrace::stack diag.filePath = fr.filePath; diag.severity = DiagnosticSeverity::Warning; diag.errCode = DescriptiveErrorCode::None; - diag.message = " [!] potential stack overflow: exceeds limit of " + - std::to_string(config.stackLimit) + " bytes\n"; + auto it = functionLocations.find(fr.name); + if (it != functionLocations.end()) + { + diag.line = it->second.first; + diag.column = it->second.second; + } + std::string message; + bool suppressLocation = false; + StackSize maxCallee = + (fr.maxStack > fr.localStack) ? (fr.maxStack - fr.localStack) : 0; + auto itLocals = functionLocalAllocas.find(fr.name); + std::string aliasLine; + if (fr.localStack >= maxCallee && itLocals != functionLocalAllocas.end()) + { + std::string localsDetails; + std::string singleName; + StackSize singleSize = 0; + for (const auto& entry : itLocals->second) + { + if (entry.first == "") + continue; + if (entry.second >= config.stackLimit && entry.second > singleSize) + { + singleName = entry.first; + singleSize = entry.second; + } + } + if (!singleName.empty()) + { + aliasLine = " alias path: " + singleName + "\n"; + } + else if (!itLocals->second.empty()) + { + localsDetails += + " locals: " + std::to_string(itLocals->second.size()) + + " variables (total " + std::to_string(fr.localStack) + " bytes)\n"; + + std::vector> named = itLocals->second; + named.erase(std::remove_if(named.begin(), named.end(), [](const auto& v) + { return v.first == ""; }), + named.end()); + std::sort(named.begin(), named.end(), + [](const auto& a, const auto& b) + { + if (a.second != b.second) + return a.second > b.second; + return a.first < b.first; + }); + if (!named.empty()) + { + constexpr std::size_t kMaxLocalsForLocation = 5; + if (named.size() > kMaxLocalsForLocation) + suppressLocation = true; + std::string listLine = " locals list: "; + for (std::size_t idx = 0; idx < named.size(); ++idx) + { + if (idx > 0) + listLine += ", "; + listLine += named[idx].first + "(" + + std::to_string(named[idx].second) + ")"; + } + localsDetails += listLine + "\n"; + } + } + if (!localsDetails.empty()) + message += localsDetails; + } + auto itPath = functionCallPaths.find(fr.name); + std::string suffix; + if (itPath != functionCallPaths.end()) + { + suffix += " path: " + itPath->second + "\n"; + } + std::string mainLine = " [!] potential stack overflow: exceeds limit of " + + std::to_string(config.stackLimit) + " bytes\n"; + message = mainLine + aliasLine + suffix + message; + if (suppressLocation) + { + diag.line = 0; + diag.column = 0; + } + diag.message = std::move(message); result.diagnostics.push_back(std::move(diag)); } } diff --git a/test/local-storage/c/extra-multiple-large-frame.c b/test/local-storage/c/extra-multiple-large-frame.c new file mode 100644 index 0000000..e5a2627 --- /dev/null +++ b/test/local-storage/c/extra-multiple-large-frame.c @@ -0,0 +1,117 @@ +#include + +int main(void) +{ + // local stack: 832 bytes + // max stack (including callees): 832 bytes + // locals: 106 variables (total 832 bytes) + // locals list: aa(8), ab(8), ac(8), ad(8), ae(8), af(8), ag(8), ah(8), ai(8), aj(8), ak(8), al(8), am(8), an(8), ao(8), ap(8), aq(8), ar(8), as(8), at(8), au(8), av(8), aw(8), ax(8), ay(8), az(8), ba(8), bb(8), bc(8), bd(8), be(8), bf(8), bg(8), bh(8), bi(8), bj(8), bk(8), bl(8), bm(8), bn(8), bo(8), bp(8), bq(8), br(8), bs(8), bt(8), bu(8), bv(8), bw(8), bx(8), by(8), bz(8), ca(8), cb(8), cc(8), cd(8), ce(8), cf(8), cg(8), ch(8), ci(8), cj(8), ck(8), cl(8), cm(8), cn(8), co(8), cp(8), cq(8), cr(8), cs(8), ct(8), cu(8), cv(8), cw(8), cx(8), cy(8), cz(8), f(8), g(8), h(8), i(8), j(8), k(8), l(8), m(8), n(8), o(8), p(8), q(8), r(8), s(8), sum(8), t(8), u(8), v(8), w(8), x(8), y(8), z(8), a(4), b(4), c(4), d(4), e(4), retval(4) + // [!] potential stack overflow: exceeds limit of 30 bytes + int a; + int b; + int c; + int d; + int e; + void* f; + void* g; + void* h; + void* i; + void* j; + void* k; + void* l; + void* m; + void* n; + void* o; + void* p; + void* q; + void* r; + void* s; + void* t; + void* u; + void* v; + void* w; + void* x; + void* y; + void* z; + void* aa; + void* ab; + void* ac; + void* ad; + void* ae; + void* af; + void* ag; + void* ah; + void* ai; + void* aj; + void* ak; + void* al; + void* am; + void* an; + void* ao; + void* ap; + void* aq; + void* ar; + void* as; + void* at; + void* au; + void* av; + void* aw; + void* ax; + void* ay; + void* az; + void* ba; + void* bb; + void* bc; + void* bd; + void* be; + void* bf; + void* bg; + void* bh; + void* bi; + void* bj; + void* bk; + void* bl; + void* bm; + void* bn; + void* bo; + void* bp; + void* bq; + void* br; + void* bs; + void* bt; + void* bu; + void* bv; + void* bw; + void* bx; + void* by; + void* bz; + void* ca; + void* cb; + void* cc; + void* cd; + void* ce; + void* cf; + void* cg; + void* ch; + void* ci; + void* cj; + void* ck; + void* cl; + void* cm; + void* cn; + void* co; + void* cp; + void* cq; + void* cr; + void* cs; + void* ct; + void* cu; + void* cv; + void* cw; + void* cx; + void* cy; + void* cz; + void* sum; + + return 0; +} diff --git a/test/local-storage/c/multiple-large-frame.c b/test/local-storage/c/multiple-large-frame.c new file mode 100644 index 0000000..08625e3 --- /dev/null +++ b/test/local-storage/c/multiple-large-frame.c @@ -0,0 +1,14 @@ +int main(void) +{ + // stack-limit: 30 + // at line 13, column 5 + // [!] potential stack overflow: exceeds limit of 30 bytes + // locals: 5 variables (total 32 bytes) + // locals list: a(4), b(4), c(4), d(4), retval(4) + int a; + int b; + int c; + int d; + + return 0; +} diff --git a/test/local-storage/c/stack-callee-caller.c b/test/local-storage/c/stack-callee-caller.c index bb1ef26..fed8e58 100644 --- a/test/local-storage/c/stack-callee-caller.c +++ b/test/local-storage/c/stack-callee-caller.c @@ -1,17 +1,31 @@ int foo(void) { + // local stack: 8192000000 bytes + // max stack (including callees): 8192000000 bytes + // at line 9, column 5 + // [!] potential stack overflow: exceeds limit of 8388608 bytes + // alias path: test char test[8192000000]; return 0; } int bar(void) { + // local stack: 0 bytes + // at line 18, column 5 + // [!] potential stack overflow: exceeds limit of 8388608 bytes + // path: bar -> foo + foo(); return 0; } -int main(void) +int mano(void) { - foo(); + // local stack: 0 bytes + // max stack (including callees): 8192000000 bytes + // at line 29, column 5 + // [!] potential stack overflow: exceeds limit of 8388608 bytes + // path: mano -> bar -> foo bar(); return 0; diff --git a/test/local-storage/c/stack-exhaustion-large-frame.c b/test/local-storage/c/stack-exhaustion-large-frame.c index e46110f..05e7762 100644 --- a/test/local-storage/c/stack-exhaustion-large-frame.c +++ b/test/local-storage/c/stack-exhaustion-large-frame.c @@ -3,6 +3,11 @@ int main(void) { + // local stack: 4096000016 bytes + // max stack (including callees): 4096000016 bytes + // at line 13, column 5 + // [!] potential stack overflow: exceeds limit of 8388608 bytes + // alias path: test char test[SIZE_LARGE]; return 0;