STIG Help

Bitcode rewrite modeling

Source-code analysis runs on an LLVM module that represents the selected analysis boundary. In non-trivial projects this boundary often excludes framework, logging, allocator, or large algorithm implementation details. The project configuration must then provide bitcode rewrite rules that replace those functions with analysis-equivalent behavior.

A rewrite is analysis-equivalent when it preserves the target function's relevant taint, data-flow, control-flow, and no-return behavior. It is not a shortcut to make the module link or the report look simpler.

Author reusable project registry rule definitions once, then select the required rules with activity transform-rule use(...) entries:

stigProject { val product = repo("product") { root = "../product" } bitcodeTransformRegistry { transfer( qualifiedFunctionName = "app::IsFinite", mode = REWRITE, from = parameter("value"), to = returnValue(), originalCompileUnit = repos["product"] / "common/math.cpp", reason = "The returned predicate depends only on the input value.", reviewStatus = NEW, ) implementation( qualifiedFunctionName = "app::Decode", mode = ADD, source = stigProject / "stubs/product/src/protocol/decode.cpp", originalCompileUnit = repos["product"] / "protocol/decode.cpp", reason = "The local stub preserves tag dispatch, buffer writes, cursor updates, and decoder error results.", ) } } // In the activity element: analyzerInput { bitcodeTransforms += { use(qualifiedFunctionName = "app::IsFinite") use(qualifiedFunctionName = "app::Decode") } }

The human-readable qualifiedFunctionName is the normal name-wide registry identity. It includes namespaces and classes but excludes the parameter list. A literal subscript operator name such as std::array<int, 3ul>::operator[] is accepted in both registry rules and activity use(...) entries. Function names reject glob, regex, namespace-only, and parameter-list syntax. One name-wide rule is required when overloads or ABI variants share the same semantic model; do not copy an LLVM symbol into mangledSymbol merely because the inventory lists several exact functions. When exact same-named functions need different models, add mangledSymbol = "_Z..." to both the project registry rule definition and its activity use(...) entry. The rule identity is then the pair (qualifiedFunctionName, mangledSymbol). STIG validates that the symbol demangles to the configured name, rejects a name-wide rule that overlaps an exact rule, and reports a specific input error when registry and use(...) selector shapes disagree. globalNamePattern remains available for non-function selectors such as groups of C++ ABI globals.

originalCompileUnit records the native compilation unit that contains the original definition matched by the rule. An activity use(...) entry selects the rule. P3 compiles its transform-owner provenance compilation unit without rewrites for source location, original LLVM metrics, and before/after reporting. Its outgoing production dependencies are not followed into the P5 analysis boundary merely because it is an owner. Real P5 implementations come from softwareUnderAnalysis and explicitly selected implementation inputs such as implementation(nativeCompileUnit = ...). stigConfigureAnalysis derives this path for new rules from the initial LLVM module; existing authored rule definitions are preserved. Normal generate/publish uses the persisted Kotlin configuration.

originalSource records the original function's source location, paired with qualifiedFunctionName and, when present, mangledSymbol. Supply a repository file such as repos["product"] / "common/math.cpp", or an HTTP (S) URL to a pinned upstream library source. Repository references retain the repository name and relative path, so the same configuration can refer to a local stub checkout and to real sources in the generating checkout. A configured link is not proof that the local file is the production implementation. An upstream reference also does not establish which distribution-specific library body was compiled.

originalSource defaults to originalCompileUnit; omit it when both refer to the same file. Set it explicitly for an implementation in a header, another source file, or an upstream URL. It supplies a source link and does not select a compiler input or change original-definition coverage.

Function rules use mode = ADD when supplying a missing definition and mode = REWRITE when replacing an existing definition. The default UNKNOWN leaves the definition decision unresolved and requires review. Migrate the former addFunctionImplementation, addNativeFunctionImplementation, and rewriteFunctionWithImplementation helpers to implementation, using source or nativeCompileUnit as appropriate. Replace the former function NOP, no-return, transfer, and configured-model helpers with nop, noreturn, and transfer.

implementation(mode = REWRITE, ...) requires originalCompileUnit: its matching definitions are converted to declarations before linking the replacement. Other rules use the same property to retain the original compilation unit for source locations and before/after evidence. There is no separate ownerCompileUnit property.

A name-wide function identity applies to every exact LLVM function that demangles to that name, including overloads and C++ ABI variants such as D0 deleting and D2 base-object destructors. Agreement in STIG's current LLVM classifier is a limited consistency check, not semantic proof. CONFLICT means established summaries have incompatible effects. Different compilation-unit owners or SSA names alone do not establish a conflict; an UNKNOWN overload leaves the combined evidence UNKNOWN. Use exact identities when variants need different models, or justify one concrete common model against every original variant. The classifier observation stays in generated evidence. An exact D0 rule cannot match D2.

Analysis harness and interface markers

Source and sink symbols are discovered from the final analysis source code after it has been compiled to LLVM IR. This final code includes the selected target compilation units plus the analysis harness, stubs, and native model sources configured for the analysis boundary. STIG scans the resulting module.ll, demangles the symbols, and selects the harness marker functions.

For field-level analysis STIG selects demangled names containing:

  • harness::SourceLeaf<;

  • harness::SinkLeaf<.

For group-level analysis it can also select:

  • harness::SourceGroup<;

  • harness::SinkGroup<.

An activity such as reference activity can be configured with AnalysisDepth.LEAF, so only the leaf variants are kept. For leaf markers, STIG parses the marker template arguments:

  • template argument 1 is the topic type;

  • template argument 2 is the harness::tags::... field path.

The interface-point annotation JSON is used only after symbol discovery. It attaches ASIL and related metadata to the already discovered marker symbols; it does not create source or sink symbols. Therefore the activity harness must materialize every analyzed source and sink by calling the harness marker functions. See Analysis harness for the user-facing harness model.

Choose and validate a function model

Follow Check and align analysis evidence for the activity's review order: resolve missing models and P5 definitions, review and confirm every active rule, then compare every sink through the analyzer and report before accepting a known gap. Use stigCheckAnalysis --configuration-only --zip --accept-partial-results to collect a failing configuration chain. P6/HTML generation is not a prerequisite for reviewing original function behavior, and a successful exploratory report does not establish semantic correctness.

Validate the concrete P5 behavior against the complete llvm/full/module.ll, including relevant callers, configured sinks and lifecycle. Original source and headers are secondary cross-checks when available; portable archives intentionally exclude them. Sufficient LLVM implementation/effect evidence can establish the model; a sufficient published library/runtime contract can also establish behavior when the original body is absent. An ambiguous ABI, incomplete contract, or missing relevant caller may require more evidence from the generating environment. Never use the rewritten P5 body as proof of what the original function did.

Choose the model that preserves the relevant source-to-sink, memory, control-flow, and return/termination behavior:

  • Preserve the configured file-level softwareUnderAnalysis boundary. Real implementations enter through that selection; outside it, supply analysis stubs or rewrite rules. Do not import individual original functions from excluded files to repair a rewrite. Changing the boundary requires a separate engineering decision.

  • Use transfer for a proved dependency, such as parameter("value") to returnValue(), or a pointer input to an output parameter. Use transfer for several dependencies. STIG resolves a C++ aggregate return lowered through LLVM sret; hidden ABI parameters do not change source-level endpoint numbering. Scalar predicates and mathematical functions normally need only their input-to-return dependencies. Select the relevant object fields explicitly; a multi-field object is not a single scalar endpoint. Use constant(...) for required literal assignments and named count fields for bounded array contributors.

  • Inspect the implementation behind any existing Kotlin helper. A short call does not make a hidden algorithm a simple transfer. Do not add models that duplicate C/C++ algorithms to avoid a source stub.

  • Use a NOP only when it preserves all behavior relevant to the configured sources and sinks, including return and memory effects. A logging-related name alone does not justify it.

  • Use a no-return rewrite only when the original function terminates the path.

  • Prefer simple Kotlin transfers and required constant writes. When they cannot express the relevant behavior, use explicit C/C++ under the STIG project's stubs/<component-or-repository>/src/<original-relative-path> or established equivalent. Keep state, ownership, virtual dispatch, conditional effects and loops visible in that stub. An existing clear implementation can guide it, but copying a whole original file does not itself reduce PhASAR analysis cost. Compile the stub with the required types and headers, then validate its P5 behavior.

REWRITE_FUNCTION_TO_UNKNOWN currently materializes the same neutral body as a NOP: ret void, or a zero/null return for supported non-void types, without argument or global writes. Hidden sret output buffers are not initialized. REWRITE_GLOBAL_TO_UNKNOWN materializes a zeroinitializer. Neither operation represents arbitrary unknown values or conservative unknown effects. Results can therefore lose original dependencies and required initialization, even if compilation and P5 structural validation pass. These are exploratory models: they remain incomplete/HIGH and require a concrete replacement before evidence-ready analysis.

Missing referenced implementations are HIGH findings, so no synthetic model is necessary merely to let provisional analysis run below the configured failure threshold. A missing function body remains a declaration: PhASAR cannot traverse it, and the result depends on applicable external-call/library models and call-to-return handling. That is different from an explicit UNKNOWN rewrite, which replaces the body as described above. Neither case establishes the missing original effects. Do not introduce an UNKNOWN rule merely to suppress the finding. A name-wide rule must preserve the behavior of every exact matched function; use exact mangledSymbol selectors when those functions need different models.

Keep each rule's definition, reason, and reviewStatus in the root project's bitcodeTransformRegistry. Activity use(...) entries select that shared definition and contain no review metadata.

reviewStatus

Meaning

Warning for a used rule

NEW

New rule; semantic review is pending. This is the default.

HIGH

MODIFIED

An existing rule changed and requires renewed review.

MODERATE

CONFIRMED

The model has been reviewed against original behavior for the recorded activity scope.

None for review status

STIG names its middle severity MODERATE. Missing models and failed P5 validation remain separate findings even when the status is CONFIRMED. UNKNOWN models cannot be confirmed. The generated llvmDerivedBehavior observation remains separate from this authored status.

Configuration initialization creates NEW rules. An automatic edit to an existing rule sets MODIFIED; unchanged regeneration preserves its status. When editing a rule or its C/C++ replacement manually, set reviewStatus = MODIFIED in the same change. There are no stored review hashes or automatic source-change freshness checks. Git records the changes; a change to originals, headers, callees, or activity assumptions still requires reconsidering affected rules.

After checking complete original LLVM or a sufficient authoritative runtime contract, all exact matches, relevant sinks/lifecycle, and current P5 evidence, record reviewStatus = CONFIRMED once on the shared rule. The first completed activity review can confirm the rule. Record the reviewed activity paths, date, evidence and assumptions in reason or an adjacent comment; no additional DSL attribute is required. Another activity can reuse that semantic review when its original behavior, ABI and assumptions are compatible, while checking its own P5 application. The number of earlier confirmations does not replace that compatibility check. The root stigConfirmFunctionRewriteRules task can apply this edit for selected rules; it records a completed review and does not perform one. See Check and align analysis evidence.

A rewrite is analysis-equivalent when it preserves which configured sources can influence which sinks, the relevant return/output dependencies, whether the call can return, and relevant framework control paths. It need not preserve algorithmic detail or metadata that cannot affect the selected analysis. The reason must state why the chosen model preserves these observations.

Several dependencies in one rule

The transfer DSL already accepts several edges. For a function with two input values and a reference output, a reviewed dependency model can be written as:

transfer( qualifiedFunctionName = "example::combine", transfer(from = parameter("first"), to = returnValue()), transfer(from = parameter("second"), to = returnValue()), transfer(from = parameter("first"), to = parameter("output")), originalSource = repos["product"] / "common/combine.cpp", reason = "Both input values contribute to the result; the first also updates the reference output.", mode = REWRITE, )

Use sources(...) and targets(...) to group edges. Every listed source contributes to every listed destination in that declaration. Several declarations form one combined summary: sources are read at function entry, each destination receives the union of its explicit contributors, and declaration order does not sequence assignments. Unspecified fields remain unchanged. A self-edge explicitly retains the old destination dependency:

transfer( qualifiedFunctionName = "example::Profile::update", transfer( from = sources(thisObject("requested"), parameter("acceleration")), to = thisObject("requested"), ), transfer(from = thisObject("requested"), to = thisObject("previous")), mode = REWRITE, )

Here previous receives the entry-time requested, with no dependency on acceleration. Grouping expands to ordinary edges in persisted configuration. Equal sources are read once; contributors to one destination result in one store. A constant assignment cannot share its destination with another contributor. These declarations do not express conditional writes or state-dependent contributor selection.

An output parameter must be a writable pointer/reference. A change to a parameter passed by value is local to the callee and is not a caller-visible output. Plain endpoints describe dependencies without specifying arithmetic, string termination, selected field offsets, initialization, or a conditional state update. Review the generated operations and the caller's later use of the value. For example, an enum-to-string function must return a pointer to string storage if a caller subsequently reads the string.

Use memory(input, byteRange(...), ..., arrays = listOf(countedArray(...))) to select pointed-to input data. Fixed ranges exclude padding; counted arrays include the count and read elements only while the count is within capacity and the element is active. Transfer arrays have no floating-point tolerance.

transfer( qualifiedFunctionName = "example::validate", mode = REWRITE, from = memory(parameter(0), byteRange(0, 4), byteRange(16, 8)), to = returnValue, originalSource = repos["product"] / "generated/validation.c", reason = "Only the three enum fields determine the return; the input is not modified.", ) transfer( qualifiedFunctionName = "example::compare", transfer(from = memory(parameter(0), byteRange(0, 25)), to = returnValue), transfer(from = memory(parameter(1), byteRange(0, 25)), to = returnValue), mode = REWRITE, originalSource = repos["product"] / "generated/records.cpp", reason = "Both objects' payload fields determine the return; padding does not contribute.", )

Selected-memory transfers currently support scalar return destinations. They lower directly to bounded LLVM byte loads and a dependency union. Reading each byte preserves dependencies from interior-field stores that PhASAR can miss with a wider load. They do not reproduce enum acceptance or numeric comparisons. Count guards preserve valid memory accesses; an analysis without path-feasibility checks can conservatively include paths through those guards. Floating-point inputs transferred to integer returns use their bit representation, avoiding the poison that an out-of-range floating-to-integer numeric conversion can produce. Generated transfer bodies retain their dependencies through P5 optimization, and final-body validation checks the configured memory reads and guards.

Parameter names and scalar fields

Use parameter("name", "path.to.scalar"), thisObject("path.to.scalar"), and returnValue("path.to.scalar") for scalar reads and assignments. Omit the field for a scalar parameter or return. A named pointer/reference parameter without a field selects its scalar payload, including a wrapper containing exactly one scalar. parameter("input") to parameter("output") can therefore copy a referenced integer or clock value without byte ranges. Configuration names parameters and fields; compiler metadata supplies their source indexes, types, and layout. For example:

transfer( qualifiedFunctionName = "example::Receiver::ForwardSlipAngle", mode = REWRITE, from = parameter("input", "slip_angle.value"), to = thisObject("slip_side_angle_"), originalCompileUnit = repos["product"] / "receiver.cpp", originalSource = repos["product"] / "receiver.cpp", reason = "Only the input slip-angle value replaces the retained slip-side-angle scalar.", ) transfer( qualifiedFunctionName = "example::Receiver::Integrity", mode = REWRITE, from = thisObject("manager.high_accuracy"), to = returnValue(), originalSource = repos["product"] / "receiver.cpp", reason = "The returned integrity depends only on the manager's high-accuracy Boolean.", )

Returned fields use the same operation, including when the C++ ABI passes a hidden sret output pointer. For a returned sample containing timestamp and velocity:

transfer( qualifiedFunctionName = "example::InterpolateSample", transfer(from = parameter("time"), to = returnValue("timestamp")), transfer(from = parameter("time"), to = returnValue("velocity")), transfer(from = parameter("first", "timestamp"), to = returnValue("velocity")), transfer(from = parameter("second", "timestamp"), to = returnValue("velocity")), transfer(from = parameter("first", "velocity"), to = returnValue("velocity")), transfer(from = parameter("second", "velocity"), to = returnValue("velocity")), mode = REWRITE, originalCompileUnit = repos["product"] / "interpolator.cpp", reason = "The timestamp copies the query time. Velocity depends on all three times and both sample velocities.", )

Every scalar in a returned object must have a transfer. Set zeroInitializeReturn = true explicitly when unassigned returned scalars must be zero, such as pitch and roll in a planar pose. This initializes those fields; it does not add contributors to them. It is rejected without named return destinations. A rule cannot mix whole-return and return-member destinations. Returning an optional object, pointer, container, or stateful object still requires a model that preserves its required effects and invariants.

Paths follow embedded classes/structs, C++ reference members and unambiguous non-virtual base classes. A path can end at a scalar or an embedded wrapper containing exactly one scalar: parameter("first", "timestamp") can therefore refer to a Clock::time_point without naming private chrono fields. For a receiver with const Time& cycle_time_, transfer(from = thisObject("cycle_time_"), to = returnValue()) reads the referenced scalar on each call. Compiler metadata supplies each reference-slot offset and pointee layout; a stored address is never substituted for the pointee value. Nested paths can cross several reference members. Writable referents can also be selected as destinations; const referents reject writes, including when their containing object is mutable. Conversely, a const receiver can contain a mutable reference. All source values and destination addresses are captured before stores, preserving aliases. These forms require a receiver or referenced parameter object; they do not support reference members in aggregate returns or counted-array operations.

By-value member reads require one LLVM scalar carrier; parameters split across multiple LLVM arguments are rejected. By-value parameters are not caller-visible write destinations.

Leaves support 8/16/32/64-bit integers and enums, Booleans, float, and double. Missing/ambiguous members, bitfields, static-member selectors, volatile/atomic accesses, virtual bases, ordinary pointer-member traversal, unions, multi-scalar leaves, and writes through const objects are rejected. A compiler-identified constructor may initialize const scalar members in its own receiver, including embedded members. This exception never permits writes through a const parameter or reference member, or writes from an ordinary method.

Fixed indices may appear in a member path, for example parameter("input", "topics[2].status") to returnValue("selected_status"), or constant(3) to returnValue("topics[2].integrity"). Compiler metadata supplies the array capacity and element layout; out-of-range indices, ordinary pointer indexing and unsupported array wrappers fail explicitly. Native arrays and wrappers containing one fixed array are supported. Each destination retains separate contributors; indexing does not imply an array-wide union. Returned arrays follow the same complete-payload coverage or explicit zeroInitializeReturn requirement as other returned members.

For a parameter-selected read, use element(parameter("input", "topics"), parameter("topic_id"), "status") as a transfer source. The optional last argument selects one scalar field in the element; omit it for a scalar array. The index is an integer or enum parameter (by value or reference), with a maximum width of 64 bits. Compiler metadata supplies the fixed capacity, stride and field offset. This reads the selected element; it does not join the other elements or reproduce an indexing algorithm in Kotlin. As with an unchecked C++ array access, the caller must supply an index within the fixed capacity. No clamping or fallback is generated. The index and payload are read before any configured destination is written. Pointer arrays, Boolean/floating indices, nested selectors and indexed destinations are rejected. Under NO_CHECK, variable-index facts can still merge according to the native analyzer's array policy; this form preserves the original indexed access. For array-wide operations, use the counted-array, corresponding-element or zero-initialization forms below. All returned payload fields must be supported, including when zero initialization is requested. Member endpoints cannot be combined with memory(...) selectors.

A getter returning an existing global object's identity can use transfer(globalAddress("_ZN3app6objectE"), returnValue()). The argument is the exact LLVM linkage name without @, taken from compiler evidence. The object must already have a definition in the analysis module; this operation does not supply its storage, initialization, virtual table or lifetime. It returns the address without loading the stored payload. Only a single source to a whole pointer return is supported. Missing objects, external declarations, thread-local objects, other address spaces, pointer unions and payload/member destinations are rejected.

Names are exact, case-sensitive source parameter names from DILocalVariable compiler records. They exclude the implicit this receiver and hidden sret output. LLVM argument spellings are not source-name evidence. An absent name, inconsistent name-to-index mapping, or a name-wide selector whose overloads use different parameter names fails explicitly. Use source indexes where necessary: parameter(0, "value") selects the same field as its named counterpart. Existing member(parameter(0), "value"), bare indexed transfers, thisObject, and returnValue remain supported. Bare indexed transfers retain their existing untyped lowering; use a named parameter or a field selector for compiler-resolved reference payloads. Named parameter endpoints belong to transfer rules, not the separate computation/memory-model API or memory(...) selectors.

The exact function's compiler debug signature resolves source parameters and the receiver. Debug types supply the member names, types, and byte locations; configuration contains no layout offsets. The analysis compiler policy includes -g -fstandalone-debug to retain complete types. A declaration-only P5 input needs an exact compiler signature and complete types. When Clang omits the declaration signature for an address-only callback, or retains a declaration signature without parameter names, STIG can use that exact function's P3 signature and parameter records. The LLVM argument/return carriers and ABI attributes, target triple, and data layout must agree. Every referenced record type must already exist completely in P5 with the same compiler identity and physical layout. No original body or guessed layout is imported. The compiler identity is the DICompositeType.identifier value inside LLVM debug metadata, not a separate generated file. A linked P3 class record can lack this value while another record retains it. STIG accepts that copy only when one identified P3 record has the same compiler source declaration, exact member-function linkage names, and complete physical layout. Missing or ambiguous identity evidence and conflicting declarations or layouts fail the import. Signature provenance is retained in !stig.member.transfer.signature.provenance; used type identities and parameter-name records survive optimization in !stig.member.transfer.types for final-body validation. Missing or mismatching evidence is rejected. Older bitcode with incomplete types must be regenerated from P2; an originalSource path alone cannot supply the missing layout.

Member transfers emit typed scalar loads and stores. Each store replaces only the selected scalar; the previous destination is not an additional contributor. Several edges read all sources before writing any destination, so swapping two members preserves both original values. Multiple contributors to one scalar form a dependency union. Scalar type changes use bit transport, including canonical Boolean storage, without reproducing numerical conversions or predicates. Use these summaries where the analysis needs dependencies rather than exact computed values. The solver's treatment of overwrites remains a separate concern: a typed store does not establish that PhASAR will remove previously recorded taint.

Required literal assignments

Use constant(0) to reset all supported scalar payload fields of a referenced object, including nested objects and fixed arrays. Compiler metadata supplies the complete layout; padding and adjacent objects remain untouched. For example:

transfer( qualifiedFunctionName = "example::clear", transfer(from = constant(0), to = parameter("messageData")), mode = REWRITE, originalCompileUnit = repos["product"] / "message.cpp", reason = "Every payload field, array element, and count is reset to zero; padding is preserved.", ) transfer( qualifiedFunctionName = "example::Receiver::SetTimepoint", transfer(from = parameter("timepoint"), to = thisObject("timepoint_")), transfer(from = constant(true), to = thisObject("received_")), mode = REWRITE, originalCompileUnit = repos["product"] / "receiver.cpp", reason = "The retained timestamp copies the input and the received flag is set unconditionally.", )

A literal assignment preserves required state even though it adds no input contributor. It is not a NOP and does not establish that the analyzer sanitizes earlier taint. constant(Long) and constant(Boolean) are supported. Boolean literals require a Boolean destination; nonzero integers require a single integer or enum scalar. Floating-point initialization supports zero only. Selecting an object or fixed array requires zero and a complete supported payload layout. Const objects (except a constructor's own initialization), by-value parameter destinations, pointers inside the selected payload, unions, and volatile/atomic fields are rejected.

A destination cannot combine a literal assignment with another contributor. Independent assignments and ordinary scalar transfers may share a rule; all input reads happen before destination writes.

Counted arrays by field name

Use the optional count field path to select active elements of an embedded fixed array. The array and its count belong to the same parameter or receiver; the compiler supplies capacity and scalar element layout, including for supported wrappers such as std::array.

transfer( qualifiedFunctionName = "example::equalSamples", transfer(from = parameter("lhs", "samples", count = "sample_count"), to = returnValue()), transfer(from = parameter("rhs", "samples", count = "sample_count"), to = returnValue()), mode = REWRITE, originalCompileUnit = repos["product"] / "samples.cpp", reason = "Both counts and both arrays' active elements contribute to the result; no input is modified.", )

parameter(index, field, count = countField) and thisObject(field, count = countField) use the same selection. Paths are relative to the selected object, so a nested array can use parameter("lhs", "validator.samples", count = "validator.sample_count").

The count contributes for empty, valid, and excessive lengths. Each array is read only when its own count is within capacity, and only its active elements are read. These are dependency summaries: they do not reproduce equality, floating-point tolerance, or short circuits between the two objects. An analyzer without path-feasibility checks can conservatively include paths through the count guards.

Named array transfers currently require one-dimensional, contiguous scalar elements and an integer count. Boolean elements, pointer traversal, dynamic containers, array writes, and aggregate-return destinations are unsupported. Other scalar inputs may contribute to the same scalar return, but literal assignments cannot share a counted-array rule. Layout, capacity, and available object bounds are checked before lowering; missing or inconsistent compiler evidence fails explicitly.

Possible array-element dependencies

Wrap a named counted-array source in elements(...) when the result depends on possible element values and the count only bounds the read. This emits compact LLVM directly, with one symbolic indexed read per array and no traversal. The number of generated instructions does not grow with array capacity.

transfer( qualifiedFunctionName = "example::Lookup::evaluate", transfer(from = parameter("query"), to = returnValue()), transfer(from = elements(thisObject("x.values", count = "x.size")), to = returnValue()), transfer(from = elements(thisObject("y.values", count = "y.size")), to = returnValue()), originalCompileUnit = repos["product"] / "lookup.cpp", mode = REWRITE, )

The count does not contribute a data dependency unless an additional scalar transfer explicitly selects it. A zero or excessive count performs no element read. If the original function terminates for an empty array, this summary needs an established nonempty-input invariant or a model preserving that exit. Bounds on the generated read do not imply precise active-prefix facts: NO_CHECK can merge all indices within the declared array capacity. Validate the abstraction against the original LLVM and the surrounding lifecycle.

Compiler metadata supplies offsets, scalar width, stride, capacity and count layout. Single-storage-member anonymous unions, including inherited ones, are supported; overlapping alternatives are rejected. The selected array must have contiguous scalar elements without padding. Ordinary scalar sources may join the same return or scalar field. For example:

transfer( qualifiedFunctionName = "example::Profile::setSegment", transfer( from = sources( parameter("query"), elements(thisObject("table.x.values", count = "table.x.size")), elements(thisObject("table.y.values", count = "table.y.size")), ), to = targets(thisObject("duration"), thisObject("snap")), ), transfer(from = parameter("delta"), to = thisObject("snap")), mode = REWRITE, )

This emits one bounded read per distinct array, shared by both destinations, and one store per destination. The delta contributes only to snap. All array and scalar source reads precede writes, including when a destination aliases a source or overwrites an array's count. Array destinations and mixing elements(...) with counted byte reductions in one rule are unsupported. Replacement-body protection and final-P5 validation must preserve the symbolic reads.

Corresponding fixed-array elements

To assign constants to selected fields of active receiver or parameter array elements, supply the array's count and use eachElement(...) as the destination:

transfer( qualifiedFunctionName = "app::Receiver::ResetActive", transfer(constant(false), eachElement(thisObject("rows", count = "count"), "active")), transfer(constant(0), eachElement(thisObject("rows", count = "count"), "elapsed")), mode = ADD, )

This form requires a void function and constant assignments into one embedded fixed array. Compiler types provide its capacity, stride, and selected scalar fields. A single count read bounds the generated loop; zero or excessive counts perform no writes. Within a valid count, only the selected fields of active elements are replaced. The count, inactive elements, other fields, and pointer identities remain unchanged. A consumer replacing an unchecked container loop must establish its valid-count invariant; excessive-count behavior is not inferred from that original loop. Constant validation follows ordinary scalar transfers, including Boolean type checks and zero-only floating-point initialization. Const destinations, pointer fields, reference traversal, overlapping destinations or count storage, noninteger counts, and mixing this form with other transfers are rejected. This operation does not construct or destroy array elements.

Use eachElement(...) on both ends when each input element supplies the corresponding output element. The optional second argument selects a scalar field inside each element:

transfer( qualifiedFunctionName = "app::CreateTrace", transfer( from = eachElement(parameter("request", "stages"), "id"), to = eachElement(returnValue("ids")), ), transfer( from = eachElement(parameter("request", "stages"), "acceleration"), to = eachElement(returnValue("accelerations")), ), mode = ADD, )

Compiler metadata supplies capacities, array strides and scalar field layouts. One bounded LLVM loop copies corresponding fields, including supported scalar conversions, without C++ wrappers or checked accessors. The generated instruction count depends on the number of field mappings, not array capacity. Different fields remain separate; this operation does not join all inputs into all output arrays.

Destinations must cover every scalar field of one aggregate sret return, or explicitly use zeroInitializeReturn = true to clear the remaining scalar members. These extra constant stores preserve initialization effects; they do not connect the array inputs to those fields. Its compiler ABI must establish noalias storage, and the source must be a referenced object. For an ADD declaration that omits noalias, STIG requires and imports this guarantee from the exact P3 definition after signature, target and compiler-type checks. It does not infer the guarantee from sret alone. All mapped fixed arrays must have the same positive capacity. Uninitialized partial returns, overlapping output fields, pointer elements, reference-member array paths, counted arrays and mixing with ordinary transfer edges are rejected. Padding is not written. Within each iteration, all input reads precede output writes; noalias keeps writes from affecting later reads. Scalar copies require matching LLVM types; float/double conversion is also supported. Integer width changes are rejected until signedness and their full transfer effects are supported.

As with other rules, verify the final LLVM and source-to-sink dependencies for the selected analyzer. Preserving element addresses in generated code does not establish the analyzer's index precision. Agreement between two results that both omit an expected dependency is not evidence of an acceptable model.

Configured value and memory models

transfer lowers typed configuration directly to LLVM. No replacement C/C++ source is generated. The same model parameters are serialized and rendered by configuration maintenance.

transfer( qualifiedFunctionName = "strlen", mode = REWRITE, model = stringLength(parameter(0)), reason = "The result is the number of bytes before the first terminating NUL.", ) transfer( qualifiedFunctionName = "example::absolute", mode = REWRITE, model = absoluteValue(parameter(0)), reason = "The result preserves the input magnitude and clears its sign bit.", ) transfer( qualifiedFunctionName = "example::finite", mode = REWRITE, model = isFinite(parameter(0)), reason = "The result is one for finite values and zero for infinity or NaN.", )

absoluteValue supports float and double. isFinite inspects the IEEE exponent bits and does not convert a floating-point value to an integer. stringLength reads one byte per iteration and carries that byte's nonzero predicate into the increment, preserving an explicit data dependency as well as the loop condition. String models require valid NUL-terminated inputs, as their library contracts do.

For strcmp, select a reviewed C implementation with implementation. Its unsigned-byte reads, first-difference/NUL stopping and returned ordering stay visible in that source. Plain parameter(...) transfers do not select a whole NUL-terminated string: numbered pointer-to-i32 return transfers read one i32 at each pointer and combine those values. They can miss a differing character beyond the first four bytes and read beyond a short string's storage. The former compareStrings DSL helper has been removed; captured STRING_COMPARE model records remain readable and mechanically validatable when reviewing older evidence.

copyMemoryAndAdvance copies a fixed number of bytes through a named pointer member, then reloads and advances that stored pointer by the copied extent. It preserves the order when the destination aliases the cursor storage: the advance uses the pointer after the copy. It copies bytes without a numerical conversion or a domain algorithm.

transfer( qualifiedFunctionName = "example::writeWord", mode = ADD, model = copyMemoryAndAdvance(parameter("value"), parameter("stream", "cursor"), bytes = 4), originalCompileUnit = repos["product"] / "stream.cpp", reason = "Copies four bytes to the current output position and advances the stored cursor.", )

The supported extents are 1, 2, 4 and 8 bytes, each lowered to one unaligned load/store followed by the pointer reload and update. Compiler metadata supplies the cursor member layout; the member must be a writable byte pointer in an embedded object. Const, volatile/atomic, non-byte and nondefault-address-space cursors are rejected. The source must be a pointer/reference parameter and the function must return void. The caller retains the original validity and capacity preconditions; this operation adds no bounds check. ADD imports the exact original compiler signature from P3 when necessary. Final-P5 validation checks the copy, extent, cursor slot, reload and update order.

bindPointerIfNull stores the incoming pointer identity in a named pointer member only when that member is null. An existing binding causes no store. returnPointerOrStoredGlobal returns the current member pointer, falling back to the current stored pointer value of an exact global symbol when the member is null. Neither operation reads or copies the pointee. For example:

transfer("example::setError", bindPointerIfNull(parameter("msg"), parameter("stream", "errmsg")), mode = ADD) transfer("example::getError", returnPointerOrStoredGlobal(parameter("stream", "errmsg"), "defaultError"), mode = ADD)

Compiler metadata supplies the embedded member layout in ADD and REWRITE modes. Setters require mutable objects and pointer slots; getters can read const objects and slots. Const pointees are supported because their contents are not accessed. Volatile/atomic slots and nondefault address spaces are rejected. The fallback must be a defined, non-TLS global pointer; its mutable stored value is loaded on every call. Final-P5 validation checks the null test, conditional store or selected return, exact member and global identities, and absence of extra memory effects.

returnAtomicStoredPointerGlobal("sharedPointer") returns a global's current stored pointer using one sequentially consistent atomic load. It requires a parameterless pointer-returning function and mutable, defined, non-TLS storage in the default address space: either a pointer or nested single-field pointer wrappers, explicitly aligned to at least the target pointer width. Integer carriers and other layouts are rejected. The model preserves the shared storage, mutable setters and returned pointee alias; it never substitutes the global's address or initial value. Final-P5 validation checks the exact symbol, atomic ordering, alignment and absence of additional effects.

Memory models use reviewed byte offsets for the target ABI. Configure only the fields actually read or written by the original function; padding is not automatically part of a payload.

transfer( qualifiedFunctionName = "example::clear", mode = REWRITE, model = zeroMemory(parameter(0), byteRange(0, 5), byteRange(8, 8)), reason = "Initialization clears the two payload ranges and preserves bytes 5 through 7 as padding.", ) transfer( qualifiedFunctionName = "example::equal", mode = REWRITE, model = equalFields( parameter(0), parameter(1), field(0, com.stigway.engine.config.BitcodeMemoryFieldType.INT32), field(4, com.stigway.engine.config.BitcodeMemoryFieldType.BYTE), field(8, com.stigway.engine.config.BitcodeMemoryFieldType.DOUBLE), ), reason = "Equality compares the three semantic fields and ignores padding.", )

zeroMemory requires a void return and nonempty, nonoverlapping ranges. It emits one LLVM memset intrinsic per range, directly; it does not generate C++ or an element-by-element C++ array loop. These constant writes preserve initialization. PhASAR's weak-store policy does not treat them as a sanitizer for earlier taint. equalFields requires two pointer/reference inputs and an integer/Boolean return. Integer fields compare their exact bit patterns. Floating-point fields use ordered equality by default: signed zeros compare equal and NaNs compare unequal.

An explicit floatingPointTolerance(absolute, relative) changes a floating-point field's comparison: finite values are equal when their distance is at most the absolute tolerance or at most the relative tolerance multiplied by the greater magnitude. If either input is nonfinite, equality compares the exact floating-point bit patterns. Choose this policy only when it matches the original implementation.

For arrays, countedArray(offset, type, capacity, countOffset, countType, tolerance) describes an unsigned element count in the same object. equalFields(..., arrays = listOf(...)) rejects unequal counts or counts beyond capacity and compares only active elements. Inactive elements and padding are not read.

transfer( qualifiedFunctionName = "example::validate", mode = REWRITE, model = validateIntegerFields( parameter(0), allowedValues(field(0, com.stigway.engine.config.BitcodeMemoryFieldType.INT32), 0, 1, 255), allowedValues(field(8, com.stigway.engine.config.BitcodeMemoryFieldType.BYTE), 0, 1), validResult = 0, invalidResult = 2, ), reason = "Validation returns zero when both enum fields are allowed, and two otherwise; it does not modify the input.", )

Ranges, capacities, constants, endpoint counts, and function types are checked before lowering. Memory extents are also checked against an available LLVM dereferenceable(...) bound. That attribute guarantees accessible bytes; it does not replace review of the complete object layout and lifetime. Unsupported signatures fail instead of receiving a neutral body.

Generated model bodies carry noinline, optnone, "no-builtins", and nounwind; their definitions are retained through llvm.used. Their scalar/memory operations, empty assembly barriers and LLVM intrinsics cannot unwind. P5 replaces invokes of these generated bodies with ordinary calls and normal branches, updating shared-handler PHIs even in protected callers. Real C/C++ replacements do not inherit this guarantee. Asynchronous exception handlers and modules with LLVM's eh-asynch flag retain their unwind edges. P5 runs ipsccp<no-func-spec> to propagate constants across function calls without creating specialized function copies. Specialization can otherwise clone a retained model and remove a constant argument, leaving a signature that no longer matches the configured transfer endpoints. Final-module validation compares all configured operations and constants after normalizing LLVM formatting. Runtime contract tests exercise the generated, optimized LLVM, including string bounds, exceptional floating-point values, padding, inactive arrays, and invalid enum values. These tool tests do not replace the activity's current P5 evidence or analyzer/sink review. Analyzer integration tests additionally check string-byte, scalar, field, and padding dependencies. The production IDE-XTaint solver deliberately uses weak stores: clearing memory preserves the runtime initialization but does not by itself sanitize pre-existing taint in the analysis.

P5 automatically expands the required array/vector/smart-pointer/address helpers inside protected C++ models. This includes address and size accessors of std::vector, std::inplace_vector and amp::inplace_vector; their nested calls must not hide element addresses inside optnone models. It first optimizes the helper bodies, then forces their expansion, while keeping the model's noinline optnone attributes. The same policy applies without explicit rewrite rules and after linking separate compilation units. A conflicting NEVER/protected replacement rule on a required helper fails explicitly. See the compiler sequence and evidence in LLVM toolchain.

Explicit native compiler-input selection

The API below selects an original native compiler input; it is not a stub or a performance rewrite. Do not use it to import selected functions from a file excluded by a file-level softwareUnderAnalysis boundary. It is relevant only to configurations that explicitly permit native implementation inputs beyond that selection:

implementation( qualifiedFunctionName = "protocol_decode", mode = ADD, nativeCompileUnit = repos["product"] / "external/protocol/decode.c", reason = "Retain tag dispatch, buffer writes, cursor updates, and the original error results.", )

This is an ADD_FUNCTION_IMPLEMENTATION rule with nativeCompileUnit, selected by an activity use(...) entry. Its compilation unit also supplies P3 provenance. P5 selects the existing P2 compiler-input record, preserving its working directory, compiler arguments, include paths, definitions, and native-action provenance. Existing explicit analysis-header configuration still applies to its selected files. External Bazel paths are resolved against the recorded compiler input, rather than compiled from a guessed path below the repository root.

The repository must be the native build's configured repository. A logical unit must match exactly one distinct recorded compiler input; missing or ambiguous records fail. No compiler recipe is synthesized for a native rule. Only implementation(source = ...) opts into the custom-source workflow. Rewrite evidence identifies a retained native implementation with providerKind = "native_compile_unit" and its original compilation-unit path. The native implementation's callees become real analyzer-input dependencies and must be accounted for in fresh P5 evidence. Selecting it does not establish that those dependencies are already resolved.

Standard-library and runtime contracts

For category membership, configured ignores, unused declarations, and the Kotlin classification source, see External symbol categories.

A portable archive can contain declarations for system-library functions without their implementation bodies. Their documented contracts can be sufficient to review a model. Useful authoritative sources include the Linux man-pages, GNU C Library manual, GNU libstdc++ manual, LLVM libc++ documentation, and the Itanium C++ ABI. Choose the source applicable to the function and captured ABI. Record the function, document section, version or retrieval date, preconditions, and the behavior used in the review. The coverage report still records the missing original definition; the semantic review records the external contract as its evidence.

For example, strcmp compares both pointed-to strings and strlen returns the position of the terminating NUL. These contracts support dependencies from string memory to the return value. A pointer dynamic_cast returns an alias of existing object storage on success and null on failure; it can adjust the address and does not copy each field. The cast's type/alias relationship and relevant failure behavior need to survive the model.

Check the generated rewrite as well as its intended dependency endpoints. A fixed-width load does not by itself establish a whole-string dependency, and an unconditional pointer return does not establish a runtime cast check. P5 structural validation alone cannot prove that optimization and the analyzer preserve the effects needed by the activity. A sufficient contract avoids unnecessary runtime-source collection; it does not bypass this verification.

P5 classifies feclearexcept, fegetexceptflag, fesetexceptflag, fetestexcept, and the GNU feenableexcept, fedisableexcept, and fegetexcept interfaces as POSIX_RUNTIME. Their fenv contract describes hardware exception flags, trap masks, and flag save/restore storage. Calls remain in the module as opaque runtime operations; classification does not supply a taint model for floating-point hardware state or verify flows through an exception callback. Keep relevant callbacks and status checks in the selected code, and review any claimed exception-derived sink flow separately.

Inspect rewrite artifacts

Final-P5 diagnostics are retained under:

activities/<activity>/build/stig/stigAnalysisP5ApplyBitcodeTransforms/ module.ll bitcode_rewrites.json bitcode_transform_log.json bitcode_transform_validation.json unresolved_application_symbols.json allowed_external_symbols.json stage_status.json problems.jsonl

bitcode_rewrites.json describes the exact rewrite applications in the module immediately after rewriting and before post-rewrite optimization, including resolved rules, exact LLVM symbols, owners, original/transformed locations, and reasons. Its functionAliasTargets map records an exact LLVM alias and the definition symbol to which that alias resolves. For example, an Itanium ABI complete-object constructor (C1) may be an LLVM alias of the base-object constructor definition (C2); that is one rewritten definition resolving both symbols, rather than two function-body rewrite applications. bitcode_transform_log.json records application and endpoint mapping. bitcode_transform_validation.json scans the final optimized P5 module and checks requested bodies that remain. When an exact application is present in bitcode_rewrites.json but its target is unmatched in the final-module validation, STIG records ELIMINATED_AFTER_P5_OPTIMIZATION; this accounts for a replacement definition removed by internalize and globaldce, rather than a missing rewrite. Read the stage status even when a final module is present: failed production retains available diagnostic outputs. Distinguish missing application definitions from audited external runtime symbols.

The activity transform_review.json records this rule count in p5BodyRuleAccounting. Its finalP5SurvivingRewrittenDefinitionRuleCount plus eliminatedAfterP5OptimizationRuleCount must equal p5InputMatchedFinalBodyRuleCount. The final-body subset includes configured function models as well as NOP, no-return, and transfer rules. P3 matches remain a separate checkpoint: NOT_IN_P5_INPUT is not optimization removal. These are counts of transform rules; exact LLVM definition and alias symbols remain separate evidence.

After P7 runs, its report-visible scope is:

activities/<activity>/build/stig/stigAnalysisP7GenerateAppliedTransformations/applied_transformations.json

This artifact has one row per final-P5 function, including unchanged functions, and records appearances in analyzer runs. It supports the Analyzer IR scope report table. It is not the activity's transform-rule review inventory; use review/transform_review.json for that inventory in a stigCheckAnalysis archive. Keep function counts and configured rule counts distinct.

Boundary-closure examples

Suppose selected activity code calls send(), while the production implementation of send() calls serialize():

  • If an active NOP or transfer transform replaces send(), that transform closes the analyzer-input boundary at send(). The original production body is provenance evidence only, so its call to serialize() does not select a transform or import the real owner of serialize().

  • If a configured replacement source defines send() and that replacement calls serialize(), the replacement is real analyzer-input code. serialize() therefore enters the next fixed-point boundary and must be supplied by another selected real unit or an active transform.

  • If the compilation unit that owns the production send() is selected in softwareUnderAnalysis, its original body enters P5 without a transform. Its call to serialize() is then a real analyzer-input dependency and must likewise be resolved. A transform must never remain active for send() while that real owner is selected.

P3 may retain an original owner solely to show before-transform provenance. Such provenance-only ownership does not make the owner's callees analyzer-input dependencies; only code that enters P5 establishes the fixed-point boundary.

Rewrite Postconditions

After bitcode rewrites are applied, STIG checks the rewritten LLVM module before running the analyzer:

  • llvm-as/llvm-dis round-tripping must produce the final .bc and .ll;

  • opt -passes=verify must accept the final module.bc;

  • debug-info and SIMD vector-store canaries run on the final module.ll;

  • configured rewrite applications are recorded before optimization in bitcode_rewrites.json;

  • surviving rewritten function bodies are checked in the final optimized module and recorded in bitcode_transform_validation.json; exact applications removed by optimization are accounted for separately;

  • the function-complete applied-transformation report is written later to applied_transformations.json.

These checks prove that the final module is structurally valid LLVM and that a configured transfer did not collapse into a no-op body after optimization. They do not prove that PhASAR/STIG's analyzer observes exactly the intended taint semantics. Analyzer semantics are covered by stig-gradle-plugin integration tests that run stig-code-analyzer on focused LLVM fixtures and assert which sinks leak and which sinks remain clean.

stigConfigureAnalysis preserves existing project registry definitions, including unselected entries. New observations never overwrite an authored model. llvmDerivedBehavior is the automatic LLVM determination in generated evidence; reviewers do not edit it, including when it remains UNKNOWN. Technical review changes the concrete model and project registry reviewStatus, independently of this compiler observation. It is no longer a registry DSL argument, and setBy is removed. Rewritten P5 bodies are never evidence for classifying their own original behavior.

LLVM facts and managed rewrite effects are deliberately separate. STIG first reads untransformed definitions, declaration attributes, return/control-flow, writes through arguments, compiler receiver/sret metadata, direct callees, and unresolved indirect calls. It then derives one conservative taint behavior and maps it as follows:

Generated LLVM observation

New rule proposal

Review implication

NO_TAINT_OUTPUT, with established empty outputEffects

nop

NEW; review required

NO_TAINT_OUTPUT, with required or unavailable effects

transfer(mode = UNKNOWN)

Preserve initialization/outputs with a concrete model

NO_RETURN

noreturn

NEW; review required; inspect relevant pre-termination effects

EXACT_TRANSFERS

transfer(mode = UNKNOWN)

Transfer candidate remains visible; explicit model required

UNKNOWN

transfer(mode = UNKNOWN)

Insufficient compiler knowledge; inspect original evidence

CONFLICT

transfer(mode = UNKNOWN)

Review incompatible established overload summaries

Resolving an unknown or conflicting model means changing it to an explicit NOP, no-return, transfer, replacement source, or compiled implementation. A common model needs an argument covering every original overload. Set the changed rule to MODIFIED and renew the affected activity evidence. Original callee/global or activity assumption changes also require reconsidering the rule; no stored digest performs this review. Changing only reviewStatus does not change the modeled LLVM behavior.

NO_TAINT_OUTPUT describes input dependencies, not absence of effects. A parameter-independent factory may initialize an aggregate through a hidden sret output pointer. Replacing it with ret void can leave the caller's copied result uninitialized. The classifier records constant stores and memset effects and handles llvm.fabs dependencies; it keeps conditional/control-dependent or unsupported memory effects UNKNOWN rather than claiming an effect-free NOP.

Memory effects alone are interpreted conservatively. For example, memory(none) does not make double tan(double) a NOP because its return still depends on its input; known unary math dependencies use curated facts. A void declaration can be a NOP when function-level LLVM memory attributes prove that it cannot write memory. Merely having no pointer output, or marking one pointer parameter readonly, does not exclude a write to global/unknown memory and therefore remains UNKNOWN. Ambiguous receiver/static status likewise remains UNKNOWN unless compiler-produced metadata or an unambiguous constructor/destructor identity resolves it.

Parameter Numbering

parameter(0) is always the first parameter written in the C++ declaration. Hidden LLVM arguments never participate in this numbering. Use thisObject for a non-static C++ receiver and returnValue for either a normal return or an aggregate return lowered through sret. STIG resolves each semantic endpoint for every exact matched function and rejects ambiguous ABI shapes instead of guessing.

Examples

Timestamp validity is a parameter-to-return dependency. The result is a boolean computed from the timestamp, so a configured return transfer is sufficient:

transfer( qualifiedFunctionName = "app::Clock::IsValid", mode = REWRITE, from = parameter(0), to = returnValue, reason = "Clock validity compares the timestamp with the zero epoch, so the boolean return depends on the timestamp parameter", )

A logging DLT extraction helper that copies an interface into a returned aggregate is an aggregate-output transfer:

transfer( qualifiedFunctionName = "app::GetFusedObjectDLTs", mode = REWRITE, from = parameter(0), to = returnValue, reason = "DLT extraction copies selected fields from the obstacle polyline list into the returned logging payload", )

A function can have several transfer edges. This example says the first and second declared parameters both taint the third declared parameter, and the first also taints the return value:

transfer( qualifiedFunctionName = "app::MergeAndPublish", transfer(from = parameter(0), to = parameter(2)), transfer(from = parameter(1), to = parameter(2)), transfer(from = parameter(0), to = returnValue), mode = REWRITE, reason = "The helper combines both input payloads into the output buffer and returns a status derived from the first input", )

Configured transfer rules model dependencies between declared parameters, thisObject, and returnValue. Dependencies on static or global state are not explicit function-endpoint edges; keep the real function or use a custom native model when such state matters to configured sources or sinks.

A function such as std::size_t GetSize() const { return sizeof(T); } is not a transfer from this to the return value. The return value depends only on the type, not on the object state. Keep the original function if possible, or use a constant-return model when available. Do not replace it with object-to-return taint.

A diagnostic helper such as const app::logging::Serializable& GetSerializable() const& { return serialize_decorator_; } does return a subobject, but the returned object is part of the logging framework. If no configured source can taint the logger/decorator object and the analysis does not observe diagnostic serialization as a sink, the function may be modeled as a no-op. The rewrite reason must say that the transfer is only through logging metadata or decorator state, not through activity payload.

A type-name helper such as:

mem::string_view GetTypename() const override { static const mem::pmr::string type_name{::project::common::visitor::logger_type_string<T>(), mem::pmr::new_delete_resource()}; return type_name; }

returns a value derived from static type metadata and allocator internals. It has no direct or indirect dependency on activity sources, so it can be a no-op for a source-to-sink payload analysis. Do not use this reasoning for a function whose returned value can contain message payload or can reach a configured sink.

Custom Native Models

Custom C++ models should be simple but type-correct:

  • include the real target headers and use target aliases/types;

  • do not re-declare generated payload layouts in the model;

  • copy only representative fields needed by the selected source/sink abstraction;

  • preserve dependency direction, not algorithmic detail;

  • avoid introducing dependencies that the original code does not have.

For example, an algorithm that converts a fan into a polyline list may be modeled by copying one representative fan element into one representative polyline element when array length is not part of the safety claim. The model still has to use the real generated container and field types, so interface layout changes remain visible at compile time.

Review Checklist

For every transformed function in applied_transformations.json, check:

  • the matched demangled symbol is the intended target;

  • the rewrite classification matches the real function's semantic role;

  • original and transformed source links point to the expected files;

  • custom models compile against the same public types as the target code;

  • transfer rules use declared C++ parameter numbering (parameter(0) is the first declared parameter);

  • generated diagnostics resolve thisObject and aggregate-return sret endpoints to the expected LLVM arguments;

  • no-op rules explain why apparent input-to-output transfers are irrelevant to the configured source/sink payload analysis;

  • each function rule names exactly the intended qualified function without regex, glob, or parameter syntax.

Use module.ll, bitcode_transform_log.json, bitcode_rewrites.json, applied_transformations.json, and component_interface_points_llvm_symbols.json to confirm the final IR shape, the report-visible transformation status, and the source/sink marker symbols that PhASAR sees.

23 October 2026