UseTraceSSA — Ordered Use-History Overlay
Overview
UseTraceSSA is an analysis-side IR overlay built on top of the Sparse Value-Flow Graph (SVFG). It enriches existing value-flow channels with explicitly ordered use histories.
UseTraceSSA makes the temporal execution order explicit for resources without relying on purely structural slices. A use creates a pseudo-version (history \(\psi\)); control-flow joins create history \(\phi\) nodes; and cycles represent loops without unrolling. Each original SVFG definition has its own channel. Local native def-use edges are routed from their consumer’s after-use version so they cannot bypass earlier uses.
Pointer addresses, memory contents, and resource state remain cleanly separated domains in this representation.
Location:
lib/IR/UseTraceSSA/,include/IR/UseTraceSSA/Namespace:
lotus::usetracessaCore Target:
CanaryUseTraceSSACore
Representation and Core Abstractions
Temporal History
TemporalHistory represents execution order once per CFG. Direct internal calls, recursion, and multiple callers share temporal ports. Instruction labels are captured in one module printing pass, preserving LLVM formatting and source locations without scanning module globals for every label.
Object Identity and Guards
Object identity lives entirely in FlowEdge::objects and GuardedEventEffect metadata. It is never a temporal node or a product-state dimension. Guarded effects keep separate event, certainty, and ObjectSet records supplied by upstream facts (such as points-to sets from AserPTA).
Unknown ObjectSet: Evaluates to
TOPand applies conservatively as May.Empty ObjectSet: Evaluates to
BOTTOMand applies to no object.Event Combination: Applicable events combine by bitwise union. Any applicable May effect or
TOPguard makes the combined event May, retaining a no-effect alternative.Must vs. May: Singleton points-to sets do not inherently imply Must semantics; underlying pointer analysis remains unchanged.
Graph size is completely independent of the number of objects in a points-to set.
Query Engine
The QueryEngine evaluates state machine queries (e.g., uninitialized use, use-after-free) against the UseTraceSSA graph. Query outcomes are Found, NotFound, and Unknown. A Found witness is a path in the supplied abstraction; it does not guarantee concrete path feasibility.
Object Query Modes
Fixed Object:
Query::memoryObjectselects one constant abstract object for the whole path.Symbolic Batch:
QueryEngine::runObjectsevaluates a finiteObjectUniversewith LLVM bit masks and returns separate Found, NotFound, and Unknown sets.
Both modes use product states of the form (FlowNodeID, AutomatonState). Sequential steps and matched call/return summaries intersect masks; alternative paths union them. Generic taint queries do not require same-object semantics.
Context Sensitivity
Queries default to context depth limit 3 (
contextLimit=3).The CLI selects call/return matching with
--context=sensitive(default) or--context=insensitive.Depth is adjusted via
--context-limit=N|unlimited(alias--context-depth).A legacy depth 0 immediately merges older call contexts but still matches the latest call site. Use
--context=insensitiveto disable call/return matching entirely.
Search Budgets
Search budgets default to unlimited. They can be constrained using --max-product-states and --max-summary-pairs (which also bounds context states). Budget exhaustion preserves existing findings but turns unfinished negatives into Unknown. The CLI prints budget statistics, warns if a budget is exceeded, and exits with code 2 for incomplete searches. Missing models and exhausted search budgets must not become safety claims.
Bug Detection
The DefectDetector provides the C++ interface for standard checkers. It supplies rules for double-free, use-after-free, memory-leak, file-leak, taint, and unchecked-use.
Property |
Recorded resource events |
Candidate-object sources |
|---|---|---|
Double-free |
Allocate, Release |
Allocate / Release |
Use-after-free |
Allocate, Release, Dereference |
Allocate / Release |
Memory-leak |
Allocate, Release, Escape, Exit |
Allocate |
File-leak |
Open, Close, Escape, Exit |
Open |
Its resource scan searches all objects and sinks symbolically, returning one concrete witness and the accepted objects per sink. Allocation resets are retained for double-free and use-after-free. With finite source guards, unrelated accesses/effects are omitted and candidate enumeration is limited to source objects.
Leak Checks
Leak rules search for an acquired resource reaching a root exit without a modeled release. They track malloc/calloc with free and fopen with fclose through root function exits. A pointer returned from a root function is treated as escaped. Ownership transfer through globals and containers is not fully modeled, so these are candidates rather than definitive leak reports. Direct release of an acquisition result is recognized as a definite close/free.
Tools and Integration
Native Construction
buildUseTraceSSAFromLotusSVFG constructs the overlay from Lotus SVFG and the LLVM module CFG. Native construction requires an explicit NativeHistoryMode. The CLI chooses this mode based on the --check flag before creating the graph.
The Full mode is an explicit general overlay for generic queries and graph dumps.
CLI Tool
The command-line tool lotus-ir-usetracessa integrates the pipeline. It builds the ICFG and SVFG, and then exports JSON/DOT, runs a structural query, or checks for potential bugs using object IDs and resource histories.
# Build the tool
cmake --build build --target lotus-ir-usetracessa
# Run a use-after-free check
build/bin/lotus-ir-usetracessa input.bc --check=use-after-free
# Export UseTraceSSA to JSON
build/bin/lotus-ir-usetracessa input.bc --format=json
Taint sources/sinks, sanitizers, and full external-call semantics require client models. Ambiguous native locations are reported as graph issues. Missing required native events produce an empty resource graph with an incompleteness issue, not a safety proof.
JSON schema 2 serializes guarded effects, using null for TOP and [] for BOTTOM. The API and CLI expose graph, query, and mask-operation statistics.
Performance and Deferred Reports
Resource construction contracts empty single-successor CFG chains, computes the
live iterated dominance frontier directly, and retains native provenance only
for event sites. Immutable object guards share their sorted storage. The scalar
LLVMHistoryBuilder::build(module) overload shares instruction printing and
operand slots across function definitions.
Bounded and context-insensitive queries expand products from reachable search states, intern call strings, and coalesce pending object deltas with shared choice proofs. Unbounded queries derive matched-call summaries from callee-entry rows instead of computing an all-pairs local closure. Query masks use inline, dense, sparse, or sparse-complement representations within their finite universe.
DefectDetector::scan(kind) retains eager witnesses. Passing
{false, true, false} retains deferred proofs, recoverable through
scan.materialize(index, graph) against the unchanged graph. Passing
{false, false, false, false} keeps sink/object counts and representatives
without expanding per-sink object lists. findAny(kind) stops after a valid
finding and records incomplete enumeration explicitly.
For checks, --quiet skips full instruction formatting, witness rendering,
and per-sink object-list expansion. --full-labels retains exact instruction
text with quiet output. Structured debug locations identify source sites;
array operands are not interpreted as locations. Only normal return ports
receive native resource Exit events. --timing reports construction subphases
and separates symbolic search from report generation.