Repository navigation
Adding a New Check
This guide walks through adding a new diagnostic check to the analyzer.
Add a new entry to DescriptiveErrorCode in include/StackUsageAnalyzer.hpp:
enum class DescriptiveErrorCode
{
// ... existing codes ...
YourNewCheck = 17 // next available number
};Update the EnumTraits<DescriptiveErrorCode> specialization in the same file:
template <> struct EnumTraits<DescriptiveErrorCode>
{
static constexpr std::array<std::string_view, 18> names = {
// ... existing names ...
"YourNewCheck"
};
};Create include/analysis/YourNewCheck.hpp:
#pragma once
#include <llvm/IR/Module.h>
#include <vector>
namespace ctrace::stack::analysis
{
struct YourNewCheckFinding
{
const llvm::Function* function = nullptr;
const llvm::Instruction* instruction = nullptr;
// ... specific finding fields ...
};
std::vector<YourNewCheckFinding> runYourNewCheck(
const llvm::Module& module,
const llvm::Function& function);
} // namespace ctrace::stack::analysisCreate src/analysis/YourNewCheck.cpp:
#include "analysis/YourNewCheck.hpp"
#include <llvm/IR/Instructions.h>
namespace ctrace::stack::analysis
{
std::vector<YourNewCheckFinding> runYourNewCheck(
const llvm::Module& module,
const llvm::Function& function)
{
std::vector<YourNewCheckFinding> findings;
for (const auto& BB : function)
{
for (const auto& I : BB)
{
// Your analysis logic here
// Iterate over instructions, check conditions, etc.
}
}
return findings;
}
} // namespace ctrace::stack::analysisAdd the new source file to STACK_ANALYZER_SOURCES:
set(STACK_ANALYZER_SOURCES
# ... existing sources ...
src/analysis/YourNewCheck.cpp
)In src/analyzer/AnalysisPipeline.cpp, add a call to your analysis module within the per-function analysis loop:
#include "analysis/YourNewCheck.hpp"
// Inside the analysis loop:
auto yourFindings = analysis::runYourNewCheck(module, function);
// Process findings...In src/analyzer/DiagnosticEmitter.cpp, add a case to convert your findings to Diagnostic structs:
Diagnostics are built with the file-local DiagnosticBuilder, not by filling the struct by hand:
for (const auto& finding : yourFindings)
{
const ResolvedLocation loc = resolveFromInstruction(finding.inst, true);
DiagnosticBuilder builder;
builder.function(finding.funcName)
.location(loc)
.severity(DiagnosticSeverity::Warning)
.errCode(DescriptiveErrorCode::YourNewCheck)
.ruleId("YourNewCheck")
.cwe("CWE-000") // omit only if the finding is not a weakness
.confidence(0.80) // optional, [0,1]
.message("\t" + std::string(prefixForSeverity(DiagnosticSeverity::Warning)) +
" description of the issue\n");
result.diagnostics.push_back(builder.build());
}Builder methods: function, filePath, severity, errCode, ruleId, confidence, cwe,
location, lineColumn, message, variableAliasing.
A CWE is what makes the finding a security result in SARIF: it becomes a rule tag and drives
security-severity. Attach one whenever the finding is a weakness.
Omit it deliberately when the finding is not — a pattern worth knowing (Recursion.Detected),
or a statement that the analysis could not conclude (*.AnalysisIncomplete). Those must not rank
among security findings. See Diagnostic Rule Catalog.
Create a test file test/your-new-check/basic-case.c:
// Expected diagnostic annotations:
// at line X, column Y
// [ !!Warn ] your diagnostic message here
void test_function(void)
{
// Code that triggers your check
}The test runner (run_test.py) parses comment annotations to validate expected diagnostics. The format is:
// at line <N>, column <N>
// [ !!Warn ] <message prefix>or for info-level:
// [!Info] <message prefix>python3 test/run_test.py --analyzer ./build/stack_usage_analyzer test/your-new-check/If your module has isolated logic worth testing, add tests to test/unit/analyzer_module_unit_tests.cpp:
// Build with: -DBUILD_ANALYZER_UNIT_TESTS=ONIf your check uses external model files (like resource-lifetime or escape analysis), define the model format and add entries to the bundled models in models/.
- New
DescriptiveErrorCodeentry +EnumTraitsupdate - Analysis module header (
include/analysis/) - Analysis module implementation (
src/analysis/) - Source file registered in
CMakeLists.txt - Wired into
AnalysisPipeline.cpp - Diagnostic emission in
DiagnosticEmitter.cpp, throughDiagnosticBuilder - A CWE attached — or deliberately omitted, for a non-weakness finding
- Facts read from the shared traversal rather than a new IR walk
- Rule documented in Diagnostic Rule Catalog and Analysis Categories
- Regression test fixture in
test/ - Tests pass with
run_test.py - Code formatted with
clang-format
- Study existing analysis modules for patterns.
DuplicateIfCondition.cppis a good simple example.ResourceLifetimeAnalysis.cppis a complex example with cross-TU support. - Use
IRValueUtils.hppfor common LLVM IR traversal utilities. - Use
LocationResolverto convertllvm::Instruction*to source locations. - The
Reachabilitymodule can filter your findings if they occur in dead code. - Keep analysis modules stateless when possible -- take the module/function as input, return findings as output.
-
Do not add another IR walk. Register an
InstructionSubscriberso your check is fed by the single traversalIRFactCollectorperforms, and take the per-module indexes fromAnalysisArtifactStoreinstead of recomputing them. See Analyzer Modules Architecture. - If the check can be left imprecise by abstract interpretation alone, consider exposing it to
SMT refinement (
--smt-rules) rather than guessing — and if it cannot conclude, say so with anAnalysisIncomplete-style rule rather than reporting nothing. - Honour the
fastprofile budgets if the check is expensive; see Analysis Profiles.