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 |
|
P3 — compile original definitions | P3 bitcode ( |
P4 — select software under analysis |
|
P5 — build code for the analyzer | Final P5 bitcode ( |
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 — | 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 — | LLVM bitcode compiled from the captured product source files and their original headers, including external product sources. Excludes build-tool compilation actions marked | 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 — | LLVM bitcode compiled from the original source files selected through | 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 — | 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 — | 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. |
Configured analyzer-input inventory — | 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 |
Optional P6 slices — | Smaller LLVM modules extracted from final P5 bitcode for particular analyzer runs when | 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:
softwareUnderAnalysisnative compilation units: the source files explicitly selected to contribute original implementation code to P5, such asactivity.cpp.Transform-owner provenance compilation units: original source files containing the function or global definitions selected by active activity transform rules. For example,
logger.cppsupplies the originallog()implementation when a rule will replacelog()with a model in P5. The rule'soriginalCompileUnitnames 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.
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
Define the element and choose whether code or authored Kotlin owns its Context interfaces. See Adding a source-backed element.
Supply the exact native target, implementation root, interface metadata, lifecycle, and
softwareUnderAnalysisselections in the element's Gradle DSL or the configuration command's options.Create or reconcile analysis configuration with the element-scoped command. It produces the required native and LLVM evidence before editing its owned configuration regions;
--previewonly writes build-directory candidates:./gradlew :activities:example_activity:stigConfigureAnalysis --preview ./gradlew :activities:example_activity:stigConfigureAnalysisStart 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:
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 { ... }:
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 { ... }:
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 { ... }:
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.
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):
Profile / DSL enum | Records |
|---|---|
| Phase timing, process resources and input identities. |
| IDE-XTaint worklist, fact, jump-function, cache and explicit-graph counts. |
| Indirect-call candidate decisions, receiver context, virtual-table slot and target symbols. |
| Process and host memory, process I/O, cgroup usage, paging and memory stalls. |
| All four profiles. It does not enable |
| 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 |
|---|---|
| Completed worklist items, not source analyses, instructions, or a completion percentage. |
| Current pending worklist items in that solver. Zero after propagation does not mean result/evidence construction has finished. |
| Updates that changed a stored jump function. |
| Stored |
| Distinct |
| Top-level flow/edge-function cache keys; normal and call-to-return edge caches count instruction-pair groups, not their nested fact pairs. |
| Distinct facts retained by this analysis's fact factory. |
| Nodes retained by the explicit exploded supergraph used for data-flow evidence. |
| Caller-root keys and the sum of keys in the loaded-argument carrier/pointee caches. |
| Distinct computed normal-flow |
| Counts of normal-cache entries with zero tuples, one unchanged-fact identity tuple, one other tuple, or multiple tuples. These partition the entry count. |
| Retained normal-flow |
| 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. |
| Time since the last snapshot publication. |
| 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:
hostSwapInMiBpsandhostSwapOutMiBpsconvert/proc/vmstatpswpin/pswpoutpage 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.hostZswapInMiBpsandhostZswapOutMiBpsseparately reportzswpin/zswpoutactivity 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.hostMemorySomeStallPercentmeasures the percentage of the interval with at least one task stalled on memory;hostMemoryFullStallPercentmeasures the percentage with all non-idle tasks stalled on memory. Both use thetotalmicrosecond counter deltas from/proc/pressure/memory, not its rolling averages.hostAvailableMiBcomes from/proc/meminfoMemAvailable.processReadMiBpsandprocessWriteMiBpsuse/proc/self/ioread_bytesandwrite_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,cgroupSwapMiBandcgroupMemoryLimitMiBdescribe the process's cgroup v2, identified bycgroupPath; they can include other processes in that group. An unlimited memory ceiling isunlimited, 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:
Run an individual stage
These commands run or reuse the prerequisites of the selected task:
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:
--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:
use Source-code analysis diagrams for paths, sinks, reachability, and feasibility;
use Code metrics and NEXY for code quantity and complexity around a function or output;
use Analysis harness for entry points and source/sink marker boundaries;
use Bitcode rewrite modeling for transfer, NOP, no-return, and custom transform semantics;
use Check and align analysis evidence when the report and implementation appear to disagree.
Ask $stig:source-analysis
For a read-only check:
For a specific analysis question: