STIG Help

Source-code analysis pipeline

STIG can connect a top-down model to selected C/C++ implementation behavior. It recompiles selected source files as LLVM bitcode using the native target's compilation commands, derives the analysis boundary, applies reviewed analysis models, runs cascading-fault and data-and-control-flow analysis, and publishes the result with the rest of the specification.

Use this workflow to answer questions such as:

  • Can data from this input reach a safety-relevant output?

  • Which calls and state transitions form the path?

  • Which external functions were modeled, and with what semantics?

  • Where does a failure stop cascading through the modeled architecture or implementation?

  • How complex is the code contributing to each output?

The native analyzer is invoked through Gradle tasks. There is no supported stig-cli or standalone analyzer CLI.

Platform support

Model authoring and report generation work on any system with JDK 21 and IntelliJ IDEA. This native analysis workflow is supported on Fedora 44 and Ubuntu 24.04; see Installation.

What the pipeline does

The Glossary defines LLVM bitcode as the binary encoding of an LLVM module: function declarations and definitions, global variables, and metadata. A compilation unit is one source file together with its included headers, compiled under a particular compiler command.

An analysis harness is code that calls the activity's lifecycle and marks analysis inputs and outputs. A stub is a simplified implementation standing in for a function or component during analysis.

Phase

What it produces and contains

P1 — native build

The native target: an executable or library built with the original compiler, such as GCC. Also records the compiler commands and a fingerprint of the native build's input files and settings.

P2 — prepare LLVM compilation commands

compile_commands.llvm.json: commands for compiling the original source files with the pinned Clang/LLVM toolchain. P2 writes commands; compilation happens in the later build tasks.

P3 — compile original definitions

P3 bitcode (module.bc): binary code compiled from the original source files selected for analysis, plus the original source files defining functions or global variables for which P5 rewrites or added implementations are configured. P3 keeps their original definitions for comparison with P5.

P4 — select software under analysis

compilation_units.txt: the source-file list selected through softwareUnderAnalysis to contribute original implementation code to P5. Files included in P3 only to inspect a function's original definition are not automatically included in this list. P4 produces a list, not binary code.

P5 — build code for the analyzer

Final P5 bitcode (module.bc): binary code compiled from the selected original implementation, analysis harness, stubs, and native model sources, with the configured function/global rewrites and LLVM optimization applied. Also produces the interface inventory of source/sink markers and a run plan naming the analyses and settings to execute.

P6 — run the analyzer

Analyzer results obtained from final P5 bitcode or a recorded slice: which analysis sources reach each sink, their data/control-flow paths, and path-feasibility results when configured.

P7 — prepare analysis reports

Diagrams and sink-result tables from P6 results; function descriptions from source comments; source-code complexity metrics and NEXY values; Markdown and diagram files for the HTML report. HTML rendering follows these tasks.

Gradle tracks each phase's declared inputs and outputs. Normal reruns reuse UP-TO-DATE or build-cache results; do not force every phase to rerun unless stale or damaged output is the subject of the investigation.

Why these native and LLVM builds exist

The native target establishes the original build; the LLVM modules make different selections and transformations inspectable. A native target can be an executable or a library. LLVM bitcode (.bc) is the binary encoding of an LLVM module, and LLVM text (.ll) is its readable representation. These are two encodings of the same checkpoint, not two separate compilations or runnable production executables.

Build / producing task

What it contains

Why it exists

Native target, often built with GCC — stigAnalysisP1BuildNativeTarget

A native executable or library containing machine code built from the original source code and its dependencies, using the native build's compiler options.

Checks that the selected target builds, creates required generated source/header files, and records the input fingerprint used by later tasks. A failed required native build stops dependent work.

Initial LLVM module, also called full LLVM — stigBuildFullLlvmOwnershipInventory

LLVM bitcode compiled from the captured product source files and their original headers, including external product sources. Excludes build-tool compilation actions marked configuration.isTool=true. P5's analysis harness, stubs, and rewrites have not been added.

Checks that these sources compile with LLVM and retains their original function definitions for behavior review, including the source file that supplied each definition. Prebuilt library implementations can still be absent.

P3 bitcode — stigAnalysisP3BuildProvenanceBitcode

LLVM bitcode compiled from the original source files selected through softwareUnderAnalysis, plus the original source files defining functions or globals selected for P5 rewrites or added implementations. Those additional files are identified by active rules' originalCompileUnit. P3 keeps the original definitions and original headers; it does not add P5's analysis harness or stubs.

Lets us inspect a function's original body before comparing it with its P5 implementation, and check which source file supplied it. A function declaration shows a function whose implementation is absent from this P3 module; that alone does not establish absence from the original native target.

P5 input before post-link rewrites — stigAnalysisP5BuildUntransformedAnalyzerInput

LLVM bitcode compiled from the selected original implementation source files, analysis harness, stubs, and native model sources. Configured source replacements have already been compiled in; post-link function/global rewrites have not yet run.

Retains the compiled code just before LLVM rewrites, so a wrong compiled replacement can be distinguished from a wrong post-link rewrite. Later rewrite tasks can reuse this compiled input.

Final P5 bitcode — stigAnalysisP5ProduceBitcodeTransformEvidence

The P5 input after configured function/global rewrites and LLVM optimization. It contains the selected implementation together with the compiled analysis harness, stubs, and models that remain after optimization.

Supplies the binary code analyzed by P6. Separate files record which rewrites were applied, whether their resulting bodies passed validation, and which required function definitions remain missing. stigAnalysisP5ApplyBitcodeTransforms checks this producer's retained status.

Configured analyzer-input inventory — stigBuildConfiguredAnalyzerInputInventory

A separate LLVM compilation of the selected original source files, analysis harness, stubs, and active implementation/model sources, including source replacements. Also writes an index mapping compiled bitcode files to source files.

Lets stigDeriveAnalysisConfiguration identify which function definitions this configuration supplies and which called functions still lack implementations, then compare them with the initial LLVM module and propose configuration changes.

Optional P6 slices — stigAnalysisP6SliceAnalyzerInput

Smaller LLVM modules extracted from final P5 bitcode for particular analyzer runs when analyzerExecution.slicingEnabled is enabled.

Reduce the code supplied to each analyzer run and record the exact input. With slicing disabled, P6 uses final P5 bitcode directly. Slicing does not recompile source code.

The LLVM compilation tasks retain module.bc and module.ll under the activity's build/stig/<task>/ directory. The final P5 producer is an exception to that naming convention: it writes both files and the rewrite evidence under build/stig/stigAnalysisP5ApplyBitcodeTransforms/. P6 slices live under build/stig/stigAnalysisP6SliceAnalyzerInput/sliced/. The native build system owns its executable/library output paths. For evidence ZIP paths, see the artifact table in Check and align analysis evidence.

With a local STIG composite build, :stig-code-analyzer:stigBuildAnalyzer also builds the native analysis tool that P6 executes. That executable contains STIG's analyzer, with its PhASAR/LLVM dependencies, and has its own build inputs and cache.

What P3 includes: a source-file example

P3 compiles two sets of original source files, using their original headers:

  1. softwareUnderAnalysis native compilation units: the source files explicitly selected to contribute original implementation code to P5, such as activity.cpp.

  2. Transform-owner provenance compilation units: original source files containing the function or global definitions selected by active activity transform rules. For example, logger.cpp supplies the original log() implementation when a rule will replace log() with a model in P5. The rule's originalCompileUnit names that file.

In this example, P3 contains compiled original code from both activity.cpp and logger.cpp. We retain the original log() body so that we can compare its behavior with the configured P5 model. If logger.cpp was included only for this comparison, that does not select its original implementation for P5. P5 compiles the selected activity code with its analysis harness, stubs, and model sources, then applies the configured LLVM rewrites.

Each compilation unit is a whole source file with its included headers, so compiling logger.cpp can also include other function definitions from that file. P3 does not recursively compile every other file called by those functions. Where a required original implementation is absent from P3, inspect the initial LLVM module before deciding that the original definition is missing. A declaration supplies a function's name and type, but no body to review.

For initialized analysis, provenanceBuild { ... } is normally empty: STIG derives these two sets from softwareUnderAnalysis and the active rules' originalCompileUnit values. P3 retains the original definitions; the analysis harness, analysis stubs, and configured replacements enter through P5.

What makes a rerun reusable

With nativeBuild { bazel { trackActionInputs = true } }, P1 asks Bazel to build and inspect the target on every invocation. Bazel reuses unchanged native work. STIG records a deterministic fingerprint of the target/options, compile actions, command arguments, action environment, output paths, and input contents, including headers and generated inputs, in build/stig/stigAnalysisP1BuildNativeTarget/native_action_inputs_fingerprint.json. The file changes only when that identity changes. Downstream tasks also track their own configuration, toolchain, harness/model sources, and other declared inputs.

This can avoid repeating expensive LLVM compilation and analysis for each activity. Identical native binary bytes alone are insufficient: comments, LLVM options, selected units, harness code, rewrite rules, or analyzer settings can change without changing the native executable. Reuse depends on the relevant task's complete input identity and available outputs/cache entries; the time saved depends on the activity. Setting trackActionInputs = false skips the native build and disables this native-input tracking.

Concurrent activity compilation

Each selected activity retains its own pipeline. Bazel commands share an exclusive Gradle service and Bazel's native build cache; each Bazel build can execute its compilation actions in parallel. Unselected activities do not become dependencies of the selected activity.

Before releasing Bazel access, stigAnalysisP1ExtractNativeCompileCommands copies the native compile actions' recorded inputs and the configured targets' declared C++ headers into its activity's compilation_inputs/ output directory. Both the native-input fingerprint and the snapshot use cquery to read CcInfo header declarations across the selected targets' dependency closure. This includes headers omitted from aquery by Bazel input discovery, including with Bazel 7. The capture includes generated sources and headers and the contents reached through Bazel symlinks. manifest.json records original paths and content hashes. These copies are Gradle outputs, and LLVM build tasks track their bytes as inputs. A missing declared input fails P1.

P2 supplies Clang commands using the captured sources, headers and working directory. Clang path mappings preserve original source names in debug information and macros. Its virtual filesystem redirects absolute native includes to captured files. LLVM builders verify compiler dependency files and reject any access to uncaptured files in the original native workspace or Bazel directories. Consumer harness/model sources remain separately tracked Gradle inputs; host tools retain the existing pinned toolchain policy.

P7 metrics and function descriptions resolve captured files through the snapshot's recorded VFS aliases. They read captured contents for comments and line numbers, while source selection, report identities and code links refer to the original files. Header aliases resolve to the same original source identity. Function description collection scans captured native headers when a snapshot is present. For a repository imported through git subrepo, source-browser paths start at its .gitrepo directory and use the recorded imported commit. CommitRef.PARENT also keeps that imported commit; the enclosing checkout's revision identifies a different repository. IntelliJ IDEA links use the enclosing checkout's actual origin and include the import directory in the local path.

P3, P5 and both LLVM inventory builders can therefore compile different activities concurrently, including while Bazel prepares another activity. Original-source inspection tasks retain exclusive access. P6 reads finalized LLVM and is scheduled independently through maxParallelAnalyzerExecutions.

The usual root stigGenerateHtml and selected activity task paths work unchanged. No cross-activity ordering rules or additional selection switch are required.

Independent Gradle invocations must not replace the same checkout's snapshots or report outputs concurrently. stigLockAnalysisWorkspace acquires an operating-system lock in .gradle/stig-analysis-workspace.lock before analysis, report generation or clean tasks can change those files, including restoration from the Gradle cache. A second invocation fails with an explanation before entering those tasks. Wait for the first invocation, or select the required activities together in one invocation. The lock stays held through downstream report tasks and is released when the build finishes; its empty file remains. This guard does not limit activity parallelism within one invocation and does not coordinate separate consumer checkouts pointing at the same native workspace.

What the original-code reference establishes

P2 converts compiler commands; it does not compile bitcode or translate a GCC executable into LLVM. The full LLVM, P3, and P5 input builders compile their own selected sources. P3 is not cut out of the linked full module, and P5 is not simply a rewritten copy of P3: its implementation selection and support sources differ.

The pinned Clang compilation resolves includes, macros, conditional compilation, overloads, and templates for those commands. LLVM text exposes the resulting operations, while debug information links them to source locations and types. The common recipe preserves that information and uses optimization/inlining and disabled vectorizers to produce operations the analyzer can interpret.

Use the initial LLVM module as the original-code reference for the captured build configuration. It does not by itself prove equivalence with GCC machine code or cover every runtime dependency: unavailable compilation actions, prebuilt libraries, and shared-library implementations can leave definitions absent. Its scope is captured product compilation units, not every repository file or the native linker's exact surviving-function set. P3 is a selected reference; absence there cannot establish absence in the full original program.

Where metrics and source comments come from

P7 reads selected source files using compilation commands. sourceCodeSelection.from, complexityMetrics.from, and functionDescriptions.from choose those command providers. With sourceCodeSelection { from(analyzerInput.compileCommands) }, the P7 branch starts at the P5 command-selection task. stigAnalysisP7CollectMetrics runs source-level metrics through Clang tooling; stigAnalysisP7CollectDescriptions extracts function documentation from source comments. Neither extracts comments from P3 bitcode or needs P3 compilation just for this purpose. P5's separate ir_function_metrics.json describes LLVM functions and is distinct from these source metrics.

The annotated task flow shows these branches, with one task per box and artifacts on the arrows. In the current initialized workflow, P6's transform-review input also brings the full and configured-input inventories into its prerequisites; they can be built or reused during a report run, as well as during explicit configuration work.

Annotated task and artifact flow

This hand-authored view records the task wiring reviewed on 2026-09-11. It shows one initialized activity with specification import, a generated harness, source metrics/descriptions, and native analysis enabled. sourceCodeSelection { from(analyzerInput.compileCommands) } supplies commands to complexityMetrics and functionDescriptions through sourceCodeSelection.compileCommands. Each box is one Gradle task; the short sentence explains its purpose.

Read A --> B as A is a prerequisite of B, the reverse of B.dependsOn(A). Task-backed input providers also create these dependencies. Arrow labels name the artifact or readiness condition consumed; they do not imply that every edge copies a file. Independent branches have no fixed relative order, and a task can reuse outputs instead of executing its action.

The view includes every P1–P7 worker and the inventory, harness, model, and report tasks needed to explain their data flow. Repeated native-fingerprint/support-file dependencies, standard Kotlin/JVM and Dokka internals, and root aggregation are omitted. The shown arrows are direct prerequisite edges; the dashed edge is conditional on a local STIG composite build, whose analyzer-build internals are not expanded. The generated complete activity task graph retains the full registered dependency detail.

Specification and activity HTML report

P7: collect source information and present analyzer results

P6: execute the analysis

P5: construct and check the program model for analysis

Original and configured inventories used by transform-review evidence

Shared interface discovery and analysis harness

P3–P4: retain selected originals and choose real implementation units

P1–P2: capture the native build and convert its commands

patched sources / completion

native build + action-input fingerprint

compile_commands.json

complete LLVM command database

complete LLVM command database

complete LLVM command database

selected original LLVM commands

provenance compilation-unit list

commands for C++ interface inspection

analysis_interface_inventory.json

original compilation recipes

softwareUnderAnalysis unit list

harness source + include directory

harness source + include directory

configured source commands

original LLVM + ownership index

configured LLVM + ownership index

exact original P3 symbols

exact P5 input symbols

configuration proposal + match inventories

transform_review_context.json

implementation + support compile commands

module before post-link rewrites

original IR + per-unit provenance bitcode

stage_status.json

unresolved / allowed symbol evidence

checked final module

final LLVM module

entry/source/sink symbols + ASILs

final module.bc

analyzer run plan

selected modules + updated run plan

symbol-completeness check

analyzer executable, local composite only

analyzer-input compilation commands

selected source commands

selected source commands

final LLVM + rewrite records

analyzed function usage

typed analyzer results

endpoint identities + ASILs

function_descriptions.json

function_metrics.json

compiled specification classes

configuration check

diagrams + presentation metadata

source complexity metrics

applied_transformations.json

IR function metrics

sink results + provisional-analysis status

analysis appendix + publication assets

analysis diagrams

model check + canonical problems

Markdown pages + report inputs

generated activity HTML

stigAnalysisP1PatchNativeSources
Apply configured native-source patches.

stigAnalysisP1BuildNativeTarget
Build the target and fingerprint its action inputs.

stigAnalysisP1ExtractNativeCompileCommands
Capture the native compiler commands.

stigAnalysisP2ConvertCompileCommandsToLlvm
Convert commands to the pinned LLVM recipe.

stigAnalysisP3SelectProvenanceCompileCommands
Select original analysis files and files with original
functions/globals for configured P5 rewrites or additions.

stigAnalysisP3BuildProvenanceBitcode
Compile those original files and headers into P3 bitcode
for comparison with P5.

stigAnalysisP4SelectSoftwareUnderAnalysis
List the softwareUnderAnalysis implementation units.

stigExtractAnalysisInterfaceInventory
Read compiled topic interfaces for import and harness generation.

stigGenerateAnalysisHarness
Generate lifecycle calls and source/sink markers.

stigBuildFullLlvmOwnershipInventory
Compile captured product sources as original LLVM evidence.

stigSelectConfiguredAnalyzerInputCompileCommands
Select implementation and support sources for the inventory.

stigBuildConfiguredAnalyzerInputInventory
Compile the configured definitions and external references.

stigDeriveAnalysisConfiguration
Compare inventories and derive a configuration proposal.

stigPrepareTransformReviewEvidence
Record original definitions and exact P3/P5 input matches.

stigAnalysisP5SelectAnalyzerInputCompileCommands
Select original implementation files, analysis harness,
stubs, and native model sources.

stigAnalysisP5BuildUntransformedAnalyzerInput
Compile and link those files into P5 input
before post-link rewrites.

stigAnalysisP5ProduceBitcodeTransformEvidence
Apply rewrites, optimize, and retain validation evidence.

stigAnalysisP5ApplyBitcodeTransforms
Check the retained P5 producer status.

stigAnalysisP5DeriveInterfacePoints
Resolve compiled entries, sources, sinks, and ASILs.

stigAnalysisP5GenerateAnalyzerRunPlan
Write the exact analyzer configurations.

stigCheckP5SymbolCompleteness
Report missing application definitions and allowed externals.

stigAnalysisP6SliceAnalyzerInput
Select final P5 or create run-specific slices.

stigAnalysisP6RunAnalyzer
Check readiness and execute the native analyzer.

stig build: :stig-code-analyzer:stigBuildAnalyzer
Build the native analysis tool when using a local composite.

stigAnalysisP7SelectSourceCompileCommands
Select source files and commands for report enrichment.

stigAnalysisP7CollectDescriptions
Extract documented function descriptions from source.

stigAnalysisP7CollectMetrics
Measure source-level function complexity.

stigAnalysisP7GenerateAppliedTransformations
Join applied rewrites with analyzed function usage.

stigAnalysisP7GenerateDiagrams
Render analyzer paths with source enrichment.

compileKotlin
Compile imported and authored specification code.

stigCheckConfig
Validate the activity configuration.

stigCheckModel
Validate the compiled specification and record problems.

stigGenerateDiagramAppendix
Assemble diagrams, sink tables, and transform evidence.

stigGenerateMarkdown
Prepare specification pages and report inputs.

dokkaGeneratePublicationHtml
Render the activity's HTML publication.

stigGenerateHtml
Validate the HTML inventory and expose the preview.

Model import is a separate source-editing operation. After refreshing the interface inventory, explicitly run stigImportModel to update Kotlin Context, then check/report it in a new invocation. The report graph does not schedule import; see Model import and export.

The full LLVM module feeds configuration derivation and original-definition evidence; it is not the input from which P3 is extracted. P3 and the P5 input compile different source selections. Likewise, P4 consumes P3's selection list, so it need not wait for P3 bitcode compilation. Source metrics and comments branch from P5's selected commands, so they need not wait for P5 rewrites or P6 execution.

For initialized analysis, StigAnalysisP6RunAnalyzerTask.transformReviewContextFile is an input backed by stigPrepareTransformReviewEvidence. That provider makes the inventory/derivation branch a P6 prerequisite even without a literal dependsOn("stigPrepareTransformReviewEvidence") at the P6 registration. This read-only proposal work does not run stigConfigureAnalysis or change the user's Gradle scripts.

Configure one source-backed element

  1. Define the element and choose whether code or authored Kotlin owns its Context interfaces. See Adding a source-backed element.

  2. Supply the exact native target, implementation root, interface metadata, lifecycle, and softwareUnderAnalysis selections in the element's Gradle DSL or the configuration command's options.

  3. Create or reconcile analysis configuration with the element-scoped command. It produces the required native and LLVM evidence before editing its owned configuration regions; --preview only writes build-directory candidates:

    ./gradlew :activities:example_activity:stigConfigureAnalysis --preview ./gradlew :activities:example_activity:stigConfigureAnalysis
  4. Start a new Gradle invocation and generate the element or complete report:

    ./gradlew :activities:example_activity:stigGenerateHtml ./gradlew :stigGenerateHtml

stigGenerateHtml runs or reuses enabled specification import, harness generation, and the P1–P7 pipeline. Required P5/P6 validations run as part of their producers. For a separate configuration review or focused symbol diagnostic, invoke:

./gradlew :activities:example_activity:stigCheckAnalysis --configuration-only ./gradlew :activities:example_activity:stigCheckP5SymbolCompleteness

The complete configuration review is an explicit diagnostic and can be expensive. Use Check and align analysis evidence for the staged activity-completion contract: complete transform configuration, current transform confirmation, every sink's stage agreement, and accepted knownGaps. A generated report alone does not establish that contract. See STIG workflows for the workflow chooser and scope. Missing referenced implementations are HIGH findings and allow provisional execution with failAt=CRITICAL. Unreviewed transform models also remain HIGH. P6 records these limitations in provisional_analysis.json and the report displays them; an absent reported flow cannot establish absence in the original program. Actual tool or LLVM failures still prevent the affected stage from completing. Exact task options come from the generated Gradle task API; configuration fields are described in Per-element configuration.

Configuration by phase

Configuration cascades from project defaults to group to element. Shared policy normally belongs in the project's Gradle build script; an activity declares its native target, softwareUnderAnalysis native compilation units, interface points, and activity transform-rule use(...) entries.

P4 — software under analysis

stigAnalysisP4SelectSoftwareUnderAnalysis emits the configured softwareUnderAnalysis native compilation-unit list for P5. Transform rules can separately supply additional implementations. P4 does not compile or link anything. The DSL owner is softwareUnderAnalysis { ... }:

softwareUnderAnalysis { include(repos["product"] / "activities/example/example_activity.cpp") includeTargetSourceTree(repos["product"] / "framework/lifecycle") }

includeTargetSourceTree intersects logical repository paths with the configured native target's compile actions. It does not scan arbitrary files or include another Bazel target solely because a path prefix matches.

P5 — analysis harness, stubs, and native model sources

The DSL owner is analyzerInput { ... }:

analyzerInput { harness { enabled = true lifecycleProfile = SourceCodeAnalysisLifecycleProfile.EXECUTE_INIT_THEN_EXPLICIT_CYCLE } stubs { enabled = true iQuotePath(repos["consumer"] / "stubs/include") stubSource(repos["consumer"] / "stubs/src/analysis_stubs.cpp") } interfacePoints { entryPoint = "example::Activity::Step()" topicsFile = repos["product"] / "activities/example/topics.json" interfacePointsAnnotationFile = repos["safety"] / "example-interface-asil.json" } bitcodeTransforms += { use(qualifiedFunctionName = "example::modeled_function") } }

For a generated lifecycle harness, P5 validates the compiled harness contract: the entry remains a noinline definition retained by llvm.compiler.used, lifecycle calls remain direct and ordered, and generated source/sink marker definitions and calls agree with the contract.

P5 symbol-completeness gate

The per-activity stigCheckP5SymbolCompleteness task classifies unresolved symbols in the final transformed module. Configured runtime/standard-library externals are allowed; missing referenced application implementations are HIGH findings. P6 depends on this check automatically. With failAt=CRITICAL, those findings permit explicitly provisional execution; invalid LLVM, tool failures, and missing required run-plan symbols remain execution blockers. The root task is an explicit aggregate over all source-analyzed activities.

This gate reports required application-definition gaps. It does not establish transform semantics or original-to-P5 agreement; review those through Check and align analysis evidence. An allowed-external category does not supply a function body or an analyzer model. Calls remain in LLVM; the selected analysis applies its available summaries and external-call handling. For example, permitting C/GNU floating-point environment functions does not model CPU exception flags, writes to saved flag objects, or exception-derived flows. When those effects matter to the requested result, they need explicit modeling or must remain a stated analysis limitation. Classifying an external dependency does not classify its application callers as runtime functions. See External symbol categories for the Kotlin classification source, category descriptions, configured-ignore defaults, and unused declarations.

P6 — analyzer execution

The DSL owner is analyzerExecution { ... }:

analyzerExecution { timeoutPerRun = 30.minutes maxAllowedRamAndSwapMiB = 16_000 maxParallelAnalyzerExecutions = 1 maxSinksPerSolver = null slicingEnabled = false dataAndControlFlowAnalysis { enabled = true } cascadingFailuresAnalysis { enabled = true } }

maxParallelAnalyzerExecutions limits native analyzer processes across the build. solverWorkers caps independent source solves inside each process; those workers share the build's logical-CPU-count-minus-one budget. Each activity executes its run-plan entries sequentially. These are separate limits: two processes with eight source workers each consume two process slots and sixteen CPU slots.

On Linux, maxAllowedRamAndSwapMiB is the memory allowance per solver worker. Before launch, the coordinator grants the minimum of the requested workers, runnable source/sink jobs, free CPU slots and available RAM divided by this allowance (rounded down). It reserves memory and CPU slots together across all activities. The process's systemd MemoryMax ceiling equals its granted workers multiplied by the allowance. If even one worker cannot fit, the run waits without holding CPU slots. It reserves 5% of physical RAM as host headroom and excludes swap from admission capacity. Reservations account for future growth even before a process allocates its memory. Resident anonymous memory already consumed by a tracked analyzer is credited against that reservation, since Linux already subtracts it from available RAM. File cache and swap are not credited. Reservations are released after process/scope cleanup, including failure and cancellation; available RAM is rechecked while waiting for other host work to finish.

For example, with 70 GiB available after headroom and a 20-GiB allowance per worker, at most three workers can be admitted across all activities. One process can receive three workers and a 60-GiB ceiling, or one can receive two workers with a 40-GiB ceiling while another receives one with a 20-GiB ceiling. A fourth worker waits. At 40 GiB per worker, a process granted three workers reserves and enforces 120 GiB. Their combined workers still obey the shared CPU budget. The worker grant stays fixed until process exit. An allowance exceeding physical RAM minus headroom fails immediately. A missing or invalid Linux memory inventory also fails admission. resource-admission-wait logs the reason for waiting, and each executed run records its reservation, allocated workers and admission wait separately from analyzer execution time. memoryPerSolverWorkerMiB and processMemoryMaxMiB distinguish the configured allowance from the resulting process ceiling in run logs.

analyzerExecution { maxAllowedRamAndSwapMiB = 40 * 1024 // 40 GiB per solver worker solverWorkers = 0 // Automatic grant from the shared CPU and memory budgets maxParallelAnalyzerExecutions = 0 // Additional process cap; automatic by default }

This changes the setting's earlier per-process meaning: existing configured values now apply per worker.

null explicitly disables the memory reservation and ceiling. On other operating systems only the existing CPU coordination applies. Reservations are shared within one Gradle invocation, not across independent builds. The memory ceiling covers all workers and shared analysis state in one process; include process overhead in the per-worker allowance. Threads do not have individual enforced limits. Exceeding the process ceiling fails that run rather than reducing its analysis scope or retrying with more memory. systemd still accounts swap separately from MemoryMax; this setting does not configure MemorySwapMax.

maxSinksPerSolver controls deterministic sink batching inside one process; null keeps all configured sinks in each source-isolated solve. Sink verdicts are ORed across covering solves, while positive reconstructed DAG components retain their solve/source/sink provenance and are never unioned into a synthetic authoritative tree.

P6 checks that its configured transform fingerprint agrees with final P5 rewrite/body-validation evidence. P6-stage inconsistency is reported by P6; it is not imported opportunistically into a different check based on a stale file in build/.

Project analysis statistics

stigGenerateHtml includes an Analysis statistics table on the project overview and a downloadable statistics.json. stigCollectAnalysisStatistics collects the same build/stig/statistics.json from existing producer artifacts without launching native analysis; it can also be run after a failed report build.

Each configured analyzed module has one row: union of P7 function line ranges, final P5 function/instruction counts (with debug intrinsics separated), distinct configured sources and ASIL A–D sinks, planned/completed native passes, per-pass and total durations, observed interned facts per solver, and sampled process RSS plus swap. P5 sizes include harnesses, models and reachable libraries. A pass can contain several source-isolated solver executions. Times include native setup and evidence generation but exclude compilation and HTML.

Select AnalyzerDiagnostic.SOLVER_GROWTH alongside the desired monitoring profiles to collect interned fact counts. The table reports their maximum in a single solver execution, not a sum of distinct facts across sources. Memory is the maximum simultaneous RSS-plus-swap sample from one process: a lower bound on its peak, without adding independently observed maxima or including the Gradle JVM. Unavailable values stay empty.

P6 retains runtime-statistics.json independently of successful analysis publication, including failed and reused execution records. Reused records keep their original times. The aggregate records the current P5 hash and excludes measurements with mismatching or unavailable input hashes from numeric totals. A successful report build is still required to render the updated HTML table; standalone collection writes JSON and logs the summary even when a native pass failed.

When collection and P5, P6 or P7 metric tasks are requested together, the collector waits for those producers. Collection by itself reads retained outputs and does not launch analysis.

Example prompt: "Using stig:source-analysis, collect the current analysis statistics, identify failed or stale passes and the largest fact/memory counts, and compare only runs with matching artifact identities."

Solver work and memory use

IDE-XTaint reuses caller traversals with the same complete transfer state and computes shared return-flow results once per call site and exit summary. Its pending worklist keeps the newest occurrence of an identical path edge and edge-function object; distinct field offsets, lifetimes and edge functions remain separate. Additional jump-function indexes are built only when a lookup needs them.

These changes reduce repeated work and retained copies of solver data. They do not bound the number of distinct analysis facts or guarantee that a large activity will finish within a particular time or memory limit. Use the monitoring records below to distinguish propagation, evidence reconstruction and teardown costs on identical inputs.

Investigating a run that does not finish

Use stig:source-analysis for a timeout, memory exhaustion, stalled propagation or an unexplained resource increase after restoring source. Preserve the complete original and the last accepted/failing variants. Temporary reductions must remain compilable and keep their required headers, state and configuration; their physical line ratio measures source restoration, not executable-line or path coverage. The goal remains the full requested original behavior.

The skill guides adaptive source reduction, expansion of passing variants, and isolation of the smallest failing addition. A single statement can expose a large amount of reachable LLVM work or an analyzer defect. Completion within a timeout does not by itself explain a sharp increase in time or memory; increasing a limit is an observation choice, not a repair.

Reuse the statistics and monitoring artifacts described here. When only the native analyzer changes, a matching saved LLVM module and run configuration allow direct native iterations without repeating user-source compilation or report generation. Changed source or other input producers require fresh input. Use the documented transformed checkpoint when validating through Gradle. Keep full-source P6 completion, semantic review and P7/HTML completion distinct. A concise attempt record is sufficient; progress plots are optional.

Example prompt: "Using stig:source-analysis, diagnose this analyzer timeout. Reduce to a passing source scope, isolate the smallest problematic addition, repair the cause within the authorized repositories, and restore the complete original source. Reuse saved matching LLVM input during native-only repairs and record time, memory and expected dependencies for each accepted increment."

Runtime monitoring

The native analyzer automatically prints phase and resource diagnostics and writes the same monitoring records to <result-stem>.monitor.log beside its analysis JSON. For example, analysis-cascading-faults-B.json has analysis-cascading-faults-B.monitor.log. During a Gradle run, both files are initially under build/tmp/stigAnalysisP6RunAnalyzer/analyzer-output/<run>/; successful P6 publication copies the run artifacts under build/stig/stigAnalysisP6RunAnalyzer/analyzer/<run>/. The existing <run>.log still captures all native output. Monitoring requires no perf permission. The defaults enable checkpoint-monitor and memory-pressure, with a 30-second interval. Select profiles in the cascading analyzerExecution DSL (defaults → group → element):

import com.stigway.gradle.dsl.AnalyzerDiagnostic import kotlin.time.Duration.Companion.seconds stig { element { analyzerExecution { diagnostics = setOf(AnalyzerDiagnostic.ALL) diagnosticInterval = 30.seconds } } }

Profile / DSL enum

Records

checkpoint-monitor/CHECKPOINT_MONITOR

Phase timing, process resources and input identities.

solver-growth/SOLVER_GROWTH

IDE-XTaint worklist, fact, jump-function, cache and explicit-graph counts.

call-resolution/CALL_RESOLUTION

Indirect-call candidate decisions, receiver context, virtual-table slot and target symbols.

memory-pressure/MEMORY_PRESSURE

Process and host memory, process I/O, cgroup usage, paging and memory stalls.

all/ALL

All four profiles. It does not enable --debug-trace.

none/NONE

Disable optional monitoring; no monitor thread or monitor log.

ALL and NONE must each be used alone; an empty selection is rejected. diagnosticInterval must be a whole number of seconds from 1 through 86400. SOLVER_GROWTH currently requires IDE-XTaint. P6 records the resolved selection and its cascade provenance in task options and forwards repeatable native --diagnostic=<profile> arguments plus --diagnostic-interval-seconds=<seconds>. These native switches also support focused developer replays of an identified module/configuration. Configure normal Gradle executions through the DSL, not by editing the P5 run plan. With NONE, essential analyzer progress, integrity errors and configured resource limits remain active; an old monitor file at the reused result path is removed so it cannot be mistaken for the new run's evidence. An inconsistent incoming fact in PhASAR's explicit graph or a failed lossless evidence round-trip stops the run as an integrity error regardless of the selected diagnostic profiles.

For a focused native timeout investigation, --debug-accepted-states prints accepted call states and their recorded primary predecessors during IDE-XTaint propagation, without waiting for a completed analysis. It is separate from monitoring profiles: neither defaults nor --diagnostic=all enable it. It writes to stderr (the complete native run log), not to the monitor sidecar or the analysis JSON. Selection options are:

  • --debug-accepted-state-min-depth=<n>: minimum access-path depth; default 5.

  • --debug-accepted-state-after-work-items=<n>: minimum completed work items; default 100000.

  • --debug-accepted-state-max-functions=<n>: distinct functions per solver execution; default 8, range 1–64.

  • --debug-accepted-state-function=<symbol>: repeatable exact LLVM function selection; omitted selects any function.

Each option requires --debug-accepted-states. One state is captured per selected function, following at most 4096 primary predecessors and printing at most 40 fact changes. Records identify the solver execution, function, instruction, entry/target facts, predecessor alternatives, and termination reason (root, cycle, step-limit, or fact-change-limit). These are accepted solver states, not temporary fact-factory allocations. The trace does not enumerate all predecessor alternatives and does not prove path feasibility. It neither changes propagation nor makes an unfinished run a valid analysis result. Use the frozen module/configuration and preserve the binary identity and full log when comparing captures; the tracing overhead is not a performance measurement.

monitor-phase-start and monitor-phase-done identify the current operation, solver execution and, for sink work, the exact physical sink instruction. IDE-XTaint separately measures initialization, propagation, result computation, diagnostic counts and solver teardown. Reconstruction, minimization, normalization, evidence interning/validation, sink teardown, ICFG construction and JSON emission also have phases. A separate monitor thread emits monitor-sample at the selected interval while a phase remains active, including during a long solver operation or destructor. It reads OS resource counters and copies published solver snapshots; it never walks mutable solver maps or interrupts the solver. Detailed sink phase records stay in the native and monitor logs; phase progress, heartbeats and summaries are also eligible for the Gradle live console.

On Linux, records include current rssMiB, current swapMiB, process-lifetime peakRssMiB, CPU time and page-fault and context-switch deltas. A heartbeat's wallSeconds and deltas cover its sample interval; phaseWallSeconds covers the active phase so far. cpuPercent=100 means approximately one CPU was busy for that interval, and process CPU use can exceed 100% with multiple threads. A large swap allocation alone does not establish active swapping. Unsupported or unavailable resource readings are labeled unavailable. With solver-growth, the analysis thread publishes an immutable snapshot at propagation start/end and after result computation. During propagation it checks the clock every 1,024 completed next() calls and publishes when the interval has elapsed:

Field

Unit and meaning

completedWorkItems

Completed worklist items, not source analyses, instructions, or a completion percentage.

pendingWorkItems

Current pending worklist items in that solver. Zero after propagation does not mean result/evidence construction has finished.

jumpFunctionUpdates

Updates that changed a stored jump function.

jumpFunctionRecords

Stored (source fact, target instruction, target fact) triples.

jumpFunctionReverseCells, jumpFunctionInlineSingletons

Distinct (target instruction, target fact) keys and the subset storing one source-fact/effect pair inline without an allocated source map. A materialized source map is retained even if later reduced to one record.

flowEdgeCacheKeys

Top-level flow/edge-function cache keys; normal and call-to-return edge caches count instruction-pair groups, not their nested fact pairs.

internedFacts

Distinct facts retained by this analysis's fact factory.

explicitGraphNodes

Nodes retained by the explicit exploded supergraph used for data-flow evidence.

callerRootCacheKeys, loadedActualCacheKeys

Caller-root keys and the sum of keys in the loaded-argument carrier/pointee caches.

normalTransferCacheEntries, normalTransferCacheHits

Distinct computed normal-flow (instruction, fact) keys and subsequent reuses, respectively. Call transfers are excluded.

normalTransferCacheKills, normalTransferCacheSingletonIdentities, normalTransferCacheOtherSingletons, normalTransferCacheMultipleResults

Counts of normal-cache entries with zero tuples, one unchanged-fact identity tuple, one other tuple, or multiple tuples. These partition the entry count.

normalTransferCacheTuples

Retained normal-flow (destination, fact, edge function) tuples, before composition with the current parent jump function.

normalTransferCacheReservedBytes

Sum of normal-cache vector capacity times tuple size, in bytes. Excludes map storage, allocator overhead and allocations owned by facts or edge functions; not process RSS.

solverSnapshotAgeSeconds

Time since the last snapshot publication.

lastProgressSnapshotAgeSeconds

Time since a published snapshot last changed the completed-work count or solver execution.

solverSnapshotExecution and sourceSymbols identify the snapshot's solver and selected source. An expensive operation can delay publication: an old snapshot is evidence of missing recent observation, not proof that the solver stopped doing work. Reading the maintained counts is constant-time; the monitor worker never reads a live solver container.

With call-resolution, each record identifies the exact instruction/IR, containing function, debug context, receiver type and virtual-table association, slot, alias-candidate count, reason and candidate target symbols. Identical decisions for a call are suppressed until the decision changes. These are intermediate candidates during call-graph construction, before any later target filtering. A temporary empty set or several targets is not itself a correctness violation. This event profile can produce substantial output on large modules; use it when tracing call resolution. Ordinary per-instruction PhASAR tracing remains a separate --debug-trace option.

Heartbeats also report host-wide swap activity and Linux memory-pressure stall time:

  • hostSwapInMiBps and hostSwapOutMiBps convert /proc/vmstat pswpin/pswpout page deltas to MiB/s using the actual system page size. These are the kernel's swap traffic counters, including zram swap devices; they are not physical disk-byte counters or bytes attributable solely to this analyzer.

  • hostZswapInMiBps and hostZswapOutMiBps separately report zswpin/zswpout activity when the kernel exposes those counters. These describe pages loaded from or stored into the zswap compressed-memory cache, expressed as uncompressed MiB/s. Zswap and zram are different mechanisms.

  • hostMemorySomeStallPercent measures the percentage of the interval with at least one task stalled on memory; hostMemoryFullStallPercent measures the percentage with all non-idle tasks stalled on memory. Both use the total microsecond counter deltas from /proc/pressure/memory, not its rolling averages.

  • hostAvailableMiB comes from /proc/meminfo MemAvailable. processReadMiBps and processWriteMiBps use /proc/self/io read_bytes and write_bytes, not counts of bytes handled by user-space read/write calls. They do not measure all host disk traffic or attribute host swap traffic to this process.

  • cgroupMemoryMiB, cgroupSwapMiB and cgroupMemoryLimitMiB describe the process's cgroup v2, identified by cgroupPath; they can include other processes in that group. An unlimited memory ceiling is unlimited, distinct from an unavailable reading. Cgroup namespaces whose path is outside the mounted hierarchy remain unavailable.

These host counters use their own memoryWindowSeconds interval, spanning successive heartbeats even when the active analyzer phase changes. They are sampled only at startup, heartbeats and shutdown to avoid adding system-wide reads to short phase boundaries. monitor-memory-summary reports their averages over the run. Missing, unreadable or decreasing counters produce unavailable, not a zero activity rate. Other processes contribute to these host values; correlate them with the analyzer's CPU, RSS and page faults. Sustained swap traffic together with substantial memory stall time indicates memory pressure consistent with thrashing; swap allocation alone does not. On hosts using zram or zswap, some swap work is compression and movement within RAM rather than disk I/O.

The memory-pressure profile warns after three consecutive sampling windows with advancing swap/zswap counters and at least 10% some stalls or 1% full stalls. Warnings are at least five minutes apart and contain the supporting rates and process measurements, plus the solver snapshot when selected. These advisory thresholds detect sustained paging with stalls; they do not establish that the analyzer caused it. Suspicion does not abort analysis. Configured hard limits and existing integrity checks retain their separate failure behavior. No numerical overhead guarantee has been established for large activities; low-frequency counters and opt-in event logging avoid per-edge log scans.

The monitor uses ordinary reads for proc files. For manual inspection with ripgrep on SELinux-enforcing systems, use rg --no-mmap '^(VmRSS|VmSwap):' /proc/<pid>/status. An rg denial of map access does not require granting that permission to the analyzer or installing an audit2allow policy module.

At exit, monitor-summary aggregates each phase. wallSeconds and cpuSeconds include nested phases; selfWallSeconds and selfCpuSeconds exclude them. Use the self times when adding different phase totals, so an execution and its propagation/evidence work are not counted twice. Every record also carries runWallSeconds from monitor startup. Records are flushed as they are written, so a stopped run retains completed phases and its latest heartbeat; a forced termination may have no final summary.

For IDE extended-taint analysis, solver-reachability includes three separately timed preparation steps: sink-observation-configuration resolves the complete configured sink universe, analysis-call-graph constructs the lazily initialized PhASAR call graph and its required alias/type information, and sink-observation-locations scans instructions for sink observations. The remaining self time covers the reachability graph walk. A large inclusive reachability time can therefore come from call-graph preparation before any sink scan or solver propagation.

monitor-input records the module and configuration paths, byte counts and SHA-256 hashes. Compare those with the recorded source revisions, analyzer settings and source/sink counts before comparing different machines. Identical inputs permit a direct timing comparison; different source files or generated Bazel dependencies require separate measurements. A P6 replay of captured final bitcode and configuration can compare analyzer behavior without copying or rebuilding the full source checkout; see Analysis checks and archived replay.

P7 — source descriptions, metrics, and diagrams

stigAnalysisP7GenerateDiagrams generates the Sigma, SVG, and Cytoscape diagrams directly from analyzer results. stigGenerateDiagramAppendix assembles the analysis appendix and publication assets.

sourceCodeSelection selects the source files from which P7 reads descriptions and computes metrics. It does not change the code that P6 analyzes:

sourceCodeSelection { from(analyzerInput.compileCommands) include("activities/example") } complexityMetrics { enabled = true }

Run an individual stage

These commands run or reuse the prerequisites of the selected task:

# Final P5 bitcode ./gradlew :activities:example_activity:stigAnalysisP5ApplyBitcodeTransforms # Native analysis, including the P5 symbol-completeness check ./gradlew :activities:example_activity:stigAnalysisP6RunAnalyzer # Analysis diagrams ./gradlew :activities:example_activity:stigAnalysisP7GenerateDiagrams

Configuration inventories and explicit maintenance

The configuration writer and explicit configuration collector have separate entry points. Their read-only inventory and proposal producers are also consumed by P6's transform-review evidence in initialized analysis, as shown above.

See the generated configuration diagram and configuration-only checking diagram for their separate task selections.

stigConfigureAnalysis selects the initial LLVM module directly for first configuration and explicit source/target overrides; reconciliation of complete persisted inputs also uses the derived configuration proposal. It is element-scoped because multiple elements can edit the shared root transform registry. Run it alone for one exact element path and review its changes before the next Gradle invocation. stigCheckAnalysis --configuration-only is read-only and has both activity and root aggregate forms. It depends on fresh proposal evidence, final P5 transform evidence and the P5 analyzer run plan, but never on stigCheckP5SymbolCompleteness, P6, or HTML generation. The captured bitcode, configurations and source/sink sidecars can then be used for a transformed-input analyzer run. Normal report generation does not invoke the configuration writer or the explicit configuration collector. It can build or reuse the full/configured inventories and proposal through stigPrepareTransformReviewEvidence → stigAnalysisP6RunAnalyzer; cache reuse applies to those tasks as well.

Analysis checks and archived replay

stigCheckAnalysis has configuration-only and full collection modes. Full collection follows the activity's stigGenerateHtml tasks and retains configuration evidence, the exact P6 inputs, analyzer results, diagrams, and report files. With --zip, it packages these files with a manifest. selected_p6_modules.json records the LLVM modules actually used by P6. The files support review; producing them does not establish agreement between the original code and the analysis results.

stigGenerateArchivedAnalysisReview creates review worksheets from retained evidence without compiling source code, running the analyzer, or regenerating HTML. stigReplayArchivedAnalyzerRun executes the captured P6 input with its captured settings. It does not construct P3 or P5 from the initial LLVM module, or regenerate P7 and HTML.

Run the analyzer from transformed inputs

Use the existing activity P6 task to measure an analyzer against retained inputs:

./gradlew :activities:example_activity:stigAnalysisP6RunAnalyzer \ --start-with=transformed \ --from=/absolute/path/to/analysis-evidence.zip \ --output-directory=/absolute/path/to/measurements/baseline \ --analyzer-executable=/absolute/path/to/stig-code-analyzer.baseline \ --console=plain --info

--from also accepts an extracted evidence directory containing evidence_manifest.json; ZIP creation is optional. The activity task selects its own exact Gradle path from a multi-module collection. Relative option paths resolve from the consumer root. Omit --run to execute all captured runs, or select one exact run name such as --run=cascading-faults-B. See the generated task reference for the option schema.

This mode removes the local P1–P5 producers, P6 slicing and transform-review producers from the task graph. Supplying --analyzer-executable also skips analyzer build/install; the executable's reported identity and SHA-256 are recorded. Without that option, the normal local-sibling/pinned analyzer resolution applies. Gradle still loads the plugin, so a changed included STIG build may compile plugin code before selecting P6.

STIG validates manifest paths, sizes and hashes before using the input. A configuration checkpoint needs final llvm/p5/module.bc and module.ll, run-plan/stig_analyzer_runs.json, and the matching configurations and .endpoints.json sidecars under run-plan/all/. New stigCheckAnalysis --configuration-only collections capture these without running P6. Older configuration ZIPs that contain only LLVM text cannot establish this executable checkpoint and are rejected with the missing input named. An arbitrary build-output directory or a loose bitcode file is insufficient.

A captured sliced run plan requires selected_p6_modules.json and its exact .bc/.ll pairs, including any P6 slices. It never substitutes the unsliced P5 module. When previous analyzer JSON is present, its recorded settings are also checked. Source/sink selections, return/parameter annotations, solver, alias analysis, call-graph mode, path-feasibility mode and sink batching remain captured inputs. Repository mappings remain as captured.

Timeout, memory limit, execution concurrency and diagnostic sampling use the invoking project's analyzerExecution settings. They are recorded separately from the captured analysis configuration. Output contains transformed_input.json, stig_analyzer_run_log.json, the required copied input files, and an analyzer/<run>/ directory with JSON, logs and sink_summary.json. Each configured sink is listed, including clean sinks. Failed runs retain their logs; the task stops at the first failed run. Measurements execute afresh even with Gradle's build cache enabled; use distinct output directories for baseline and candidate runs. A populated destination must already belong to this task.

These are diagnostic results kept outside normal P6/report outputs. Finishing successfully establishes execution and output validity; findings such as cascading faults or missing expected flows still need review. A diagnostic result does not become a current P7/HTML report. Gradle rejects a combined transformed-P6 and P7/report invocation for the same activity, so existing local results cannot silently stand in for the diagnostic run. Omit all transformed-input options for the ordinary local pipeline. full and reduced starting checkpoints remain unsupported.

Use Check and align analysis evidence for collection options, review steps, and the limits of archived replay.

Read the result

For an existing-analysis check or repair, follow the staged evidence workflow above. For ordinary reading of an already generated report, start with its result and use these topics for detail:

Ask $stig:source-analysis

For a read-only check:

Use $stig:source-analysis to check the automated cascading and data-flow analysis configuration for <module path>. Keep this read-only: inspect the current P1-P5 evidence, exact compilation units, transforms, and unresolved symbols; do not edit analysis configuration.

For a specific analysis question:

Use $stig:source-analysis to determine whether <source/interface> can reach <sink/output> in <module path>. Use current generated evidence, distinguish structural reachability from feasibility proof, and cite the exact task artifacts and LLVM symbols.
23 October 2026