Skip to content

Adding a New Check

Hugo edited this page Sep 25, 2026 · 2 revisions

Adding a New Check

This guide walks through adding a new diagnostic check to the analyzer.


Step 1: Define the Error Code

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"
    };
};

Step 2: Create the Analysis Module

Header

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::analysis

Implementation

Create 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::analysis

Step 3: Register in CMakeLists.txt

Add the new source file to STACK_ANALYZER_SOURCES:

set(STACK_ANALYZER_SOURCES
    # ... existing sources ...
    src/analysis/YourNewCheck.cpp
)

Step 4: Integrate into the Pipeline

Wire into AnalysisPipeline

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...

Emit Diagnostics

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.

Choosing a CWE

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.


Step 5: Add Tests

Regression Test Fixture

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>

Run Tests

python3 test/run_test.py --analyzer ./build/stack_usage_analyzer test/your-new-check/

Unit Tests (optional)

If your module has isolated logic worth testing, add tests to test/unit/analyzer_module_unit_tests.cpp:

// Build with: -DBUILD_ANALYZER_UNIT_TESTS=ON

Step 6: Update Models (if applicable)

If 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/.


Checklist

  • New DescriptiveErrorCode entry + EnumTraits update
  • 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, through DiagnosticBuilder
  • 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

Tips

  • Study existing analysis modules for patterns. DuplicateIfCondition.cpp is a good simple example. ResourceLifetimeAnalysis.cpp is a complex example with cross-TU support.
  • Use IRValueUtils.hpp for common LLVM IR traversal utilities.
  • Use LocationResolver to convert llvm::Instruction* to source locations.
  • The Reachability module 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 InstructionSubscriber so your check is fed by the single traversal IRFactCollector performs, and take the per-module indexes from AnalysisArtifactStore instead 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 an AnalysisIncomplete-style rule rather than reporting nothing.
  • Honour the fast profile budgets if the check is expensive; see Analysis Profiles.

Clone this wiki locally