fold/docs/research/symbolic-modeling-research-2026-06-03.md
2026-06-04 20:20:29 -04:00

255 lines
17 KiB
Markdown

# Fold symbolic modeling research note — solver state, guards, memory, CFG
Date: 2026-06-03
Purpose: summarize the research pass requested after the YAMS null/div false-positive triage. The goal is not to turn Fold into angr, KLEE, CBMC, or CSA, but to borrow the mature pieces that address Fold's current weak spots: deeper guard propagation, path-sensitive CFG joins, expression identity, memory/access-path modeling, and explicit preconditions.
## Research inputs
- Sage/tool routing was checked first; no project-specific Sage route suggestions were returned.
- PaperBridge CLI was used because no PaperBridge MCP server was visible in the Sage hub. Relevant hits included:
- Dai et al., *A Memory Model for Symbolic Execution*, DOI `10.1109/ifcsta.2009.11`.
- KLEE / path-condition papers, including Cadar et al., *KLEE: Unassisted and Automatic Generation of High-Coverage Tests for Complex Systems Programs*.
- Static null-pointer/path-sensitive analysis literature, including PANDA/Pulse-related results.
- Web search covered angr docs, KLEE OSDI paper, and Clang Static Analyzer ProgramState/ConstraintManager docs.
- GitHub/source evidence was inspected for:
- angr `SimState`, solver plugin, and memory concretization strategy code.
- LLVM Clang Static Analyzer `ProgramState.h` and `ConstraintManager.h`.
- CBMC symbolic-execution equation/guard code via code search.
- Infer Pulse docs/source snippets for memory model and latent-vs-manifest errors.
## What mature analyzers consistently separate
### 1. Program state is more than a variable map
Fold currently has a useful `SymbolicEnv`: current variable expressions, array/heap arrays, member cache, ranges, non-null marks, a `path_` predicate, precision tags, observer pointer, and a heap model. This is close to a state object, but the APIs still make it too easy to manually push/restore `path_` and to add detector-specific side tables.
Mature tools make the state boundary explicit:
- Clang Static Analyzer `ProgramState` is documented as the tuple of `Environment` (expression values), `Store` (locations), and symbolic constraints / generic client data. It is functional/persistent, and infeasible states are represented as `nullptr`.
- angr `SimState` registers memory/register/solver plugins and exposes `add_constraints`, `satisfiable`, `copy`, `step`, and `merge` as state operations. A `_global_condition` is applied to added constraints, memory reads, and related operations.
- KLEE `ExecutionState` carries its constraint set and address space together; path forking copies the state and extends each child with its branch condition.
**Implication for Fold:** make `SymbolicState` / `PathState` a first-class wrapper around `SymbolicEnv` instead of letting detectors and walkers manipulate path, memory, and side facts independently. `SymbolicEnv` can remain the implementation detail.
### 2. Branching is an `assume` operation, not ad hoc path concatenation
Clang CSA exposes `assume(cond, true/false)` and `assumeDual(cond)` through `ConstraintManager`; each return value is either a feasible new state or null. The header comments explicitly define this as the principal mechanism for path-sensitivity. CBMC records guarded SSA steps and later converts guards into solver implications. KLEE forks states and extends each child's path condition. angr likewise adds constraints to a state and has explicit state merge conditions.
**Implication for Fold:** create one path API:
```cpp
struct AssumeResult { optional<State> truthy; optional<State> falsy; };
AssumeResult assume(State s, z3::expr condition);
optional<State> assume(State s, z3::expr condition, bool polarity);
State join(State then_state, State else_state, z3::expr condition);
```
Every `if`, loop guard, switch edge, short-circuit operator, ternary, and P-Code `CBRANCH` should use this API. The recent loop-unroll fix is exactly the kind of bug this API should prevent by construction.
### 3. Guards should remain attached to facts and effects
CBMC's equation model records assignments, assumptions, constraints, and gotos with guards, then emits implications such as `guard => constraint`. This avoids losing the branch condition when the analysis later joins paths.
Fold currently joins variable values with `ite` in some places and stores some non-null/range facts in side maps. This works for simple cases but is fragile for:
- facts learned under a guard but consumed after nested joins;
- early-return guard shapes;
- switch/case paths;
- loop body effects that are only valid under the loop guard;
- member/call-product aliases used as div-by-zero subjects.
**Implication for Fold:** keep guarded facts as first-class data:
```cpp
struct GuardedFact {
z3::expr guard;
FactKind kind; // NonNull, Range, Equality, SizeRelation, etc.
AccessPath subject;
z3::expr value_or_predicate;
};
```
At a sink, prove/refute by querying `state.path && fact.guard && sink_condition`. At a join, either retain both guarded facts or produce a safe merged fact only when both arms prove the same property.
### 4. Memory needs region/access-path identity, not just expression text
angr models memory through state plugins and uses explicit read/write concretization strategies for symbolic addresses. Clang CSA uses `MemRegion` plus store bindings and `SVal` values. Infer Pulse models stack variables as abstract addresses and heap edges such as `addr --field/access--> addr2`; unknown calls scramble reachable state optimistically to avoid false positives. KLEE keeps memory objects in the state's address space and reasons over object bounds.
Fold has stack/local bindings, arrays, heap-array helpers, buffer-size trackers, a heap model, and a member cache. The weak point is identity: member expressions, implicit `this`, repeated `.size()`/`.empty()` calls, and pointer aliases can still be modeled by cache keys or fresh expressions that are too narrow.
**Implication for Fold:** define canonical access paths:
```cpp
struct RegionKey { FunctionFrame frame; DeclOrSynthetic object; AllocationSite alloc; };
struct AccessPath { RegionKey base; vector<Access> edges; };
// Access = Field(name), Deref, Element(index_expr), CallResult(summary_id), SizeOf, LengthOf, ...
```
Use `AccessPath` as the subject key for non-null marks, range facts, member values, call-summary returns, and report fact IDs. Textual expression hashes can remain a fallback, not the primary identity.
### 5. Unknown/external calls should have explicit havoc/precondition semantics
Infer Pulse's docs are useful here: Pulse reports a null dereference when all conditions are manifest, treats some parameter-dependent bugs as latent, and makes optimistic assumptions about unknown calls by scrambling reachable state. This is a better model than either suppressing all external-precondition cases or reporting them as equal-confidence bugs.
**Implication for Fold:** separate:
- **Manifest finding:** sink reachable under current state without external preconditions.
- **Latent/precondition finding:** sink requires a caller-provided condition, e.g. `param != nullptr`, `size > 0`, `callback returns non-null`.
- **Unknown-call havoc:** unknown function invalidates/scrambles reachable memory and facts according to a policy, rather than preserving stale facts or globally dropping the path.
This maps cleanly to Fold's confidence tiers: latent/precondition findings should not be `Proved` unless a caller or contract discharges the precondition.
## Tool-by-tool lessons for Fold
| Tool | Relevant mature idea | What Fold should adopt | What Fold should not adopt now |
|---|---|---|---|
| angr | Plugin-based `SimState`; constraints via solver plugin; merge conditions; memory concretization strategies | Explicit state operations, merge conditions, symbolic-address policy, future binary/P-Code path exploration | Full Python-in-process engine or full binary symbolic execution as the default scan lane |
| KLEE | `ExecutionState` = constraints + address space; fork on branches; generate concrete witnesses | Optional confirmation adapter; entry-reachability validation; object/bounds mental model | Embedding KLEE into source scanning; blocking on KLEE availability |
| Clang Static Analyzer | Immutable `ProgramState`; `Environment` + `Store` + constraints; `assumeDual`; null state means infeasible | `assume` API, functional state snapshots, persistent side facts, null/infeasible child pruning | Reimplementing the full CSA engine |
| CBMC | Guarded SSA equation; assignments/constraints/gotos carry guards; joins via guarded implications | Guarded facts/effects and better join semantics | Whole-program bounded model checking as Fold's default scanner |
| Infer Pulse | Access-path heap graph; latent vs manifest bugs; optimistic unknown-call havoc | Precondition classification, access-path memory, unknown-call policy | Under-approximate-only reporting for all Fold detectors |
| CodeQL | Declarative dataflow and query-level precision controls | Useful for benchmark comparison and detector design vocabulary | Replacing Fold's solver-backed sink queries |
## Recommended Fold architecture target
### Core state type
Introduce a small source-lane state facade without ripping out `SymbolicEnv`:
```cpp
class SourcePathState {
SymbolicEnv env;
z3::expr path;
GuardedFactSet facts;
MemoryModel memory;
PrecisionTag precision;
Trace trace;
};
```
Initial implementation can store `path` in `env` for compatibility, but all new CFG code should go through `SourcePathState` helpers.
### State operations
- `assume(cond)` returns feasible true/false children after a bounded solver check.
- `addFact(kind, subject, predicate)` records fact under current path.
- `assign(subject, value)` bumps SSA/version and kills dependent facts.
- `load/store(access_path)` routes memory through region/access-path identity.
- `join(cond, then, else)` merges bindings, memory, non-null, ranges, and facts using guarded data, not detector-specific heuristics.
- `havoc(reachable_from_args, policy)` handles unknown calls.
### Detector contract
Detectors should ask state questions instead of recomputing guards:
- `isProvedNonNull(subject)`
- `canBeNull(subject)`
- `isProvedNonZero(subject)`
- `canBeZero(subject)`
- `range(subject)`
- `witness(sink_condition)`
This keeps null-deref/div/oob/sql detectors from each inventing a partial path model.
## Phased implementation plan
### Phase 0 — state API shim, no behavior change
- Add `source_path_state.hpp/cpp` wrapping `SymbolicEnv` path operations.
- Route `SecurityWalker` path push/restore through `assume`/`withPath` helpers.
- Add unit tests that assert nested `if`, `switch`, and loop paths call the shared API.
### Phase 1 — feasible-child pruning everywhere
- Use bounded solver checks for `if`, `for/while` guard, switch case, ternary, logical `&&`/`||`.
- Preserve current budgets; on timeout degrade to imprecise rather than abort.
- Regression targets: YAMS reported files, existing loop OOB fix, switch/case div guards.
### Phase 2 — canonical subjects/access paths
- Implement `AccessPath` for:
- local variables;
- implicit/explicit `this` fields;
- simple pointer deref and field chains;
- zero-arg stable query calls (`size`, `empty`, `length`, `count`).
- Move member cache, non-null marks, and range facts toward `AccessPath` keys.
- Keep existing string/cache fallback while measuring hit/miss rates.
### Phase 3 — guarded facts and joins
- Store non-null/range/equality facts as guarded facts.
- Join facts by retaining guarded alternatives first; only collapse to unguarded facts when both arms prove the same fact.
- Use guarded facts for div-by-zero product aliases and null deref after early returns.
### Phase 4 — external preconditions and unknown-call havoc
- Add report evidence field for `precondition_required` / `latent` findings.
- Model unknown calls with explicit havoc of reachable memory/facts.
- Add local contracts for common safe APIs and Fold-local `fold_assume_*` intrinsics.
### Phase 5 — binary/P-Code CFG state exploration
- Apply the same state/assume/join concepts to P-Code `CBRANCH` and machine div/rem gates.
- Keep raw binary string/symbol scans independent.
- Do not lift binary into the source IR; use shared concepts, not shared representation.
## Implementation checkpoint — 2026-06-04
Completed PBI-012 modeling increments:
- Phase 0/1: `SourcePathState` shim plus bounded UNSAT-only pruning for source `if`, loop guards, switch cases, short-circuit RHS expressions, and ternary arms.
- Phase 2: canonical `AccessPath` subject identity and additive AccessPath-backed member/range/non-null caches.
- Phase 3: standalone guarded-fact substrate with guarded joins and branch-specific alternatives.
- Phase 4: latent/precondition evidence fields in JSON/SARIF and detector emission for parameter-dependent null/div callsites; conservative unknown-call havoc for mutable reference/address/pointer-reachable facts now clears AccessPath-backed member/range/non-null facts.
- Phase 5: P-Code path sensitivity now includes bounded forward branch exploration: `CBRANCH` forks taken/fallthrough paths, forward `BRANCH` jumps stop local fallthrough, and `RETURN`/indirect branches terminate the current path without broadening bare symbolic binary divisor reporting.
- Follow-up review fixes: hybrid profile merging now preserves source/binary timing and prefilter counters; assertion SIGABRTs inside source-TU analysis are treated as invariant failures rather than silently quarantined TUs.
Latest focused YAMS shard delta:
- `/tmp/fold-yams-reported-v7.json`: 23 results (`10 div-by-zero`, `13 null-deref`).
- `/tmp/fold-yams-reported-v8.json`: 20 results (`7 div-by-zero`, `13 null-deref`).
- Removed: `post_ingest_enrichment.cpp:91`, `post_ingest_enrichment.cpp:92`, `sqlite_vec_backend.cpp:3140`.
- No new results relative to v7.
Validation gates exercised during the checkpoint:
- Source/modeling: `fold:access_path`, `fold:source_path_state`, `fold:guarded_facts`, `fold:div_by_zero`, `fold:null_deref`, `fold:oob_access`, `fold:scan_e2e`.
- Binary/P-Code: `fold:pcode_encoder`, `fold:pcode_summary`, `fold:binary_backend`, `fold:scan_e2e`.
- Hygiene: `lsp_diagnostics` on changed C++ files with 0 errors, plus `git diff --check`.
- Full-suite gates: initial checkpoint passed via monitor `#2` (`86/86 OK`); follow-up branch/havoc/evidence/profile fixes passed `meson test -C build/release --print-errorlogs && git diff --check` via monitor `#3` (`87/87 OK`, exit `0`).
## Benchmark and validation gates
- Keep the existing source/binary/hybrid/Rust/Swift goldens.
- Add a Fold production self-scan guard for null/div/oob deltas.
- Keep the YAMS focused shard as a precision guard:
- `/tmp/yams-reported-compile_commands.json`
- latest meaningful report: `/tmp/fold-yams-reported-v8.json` with 20 null/div findings.
- baseline report: `/tmp/fold-yams-reported-v7.json` with 23 null/div findings.
- For each modeling phase, classify the shard findings into:
- true positive;
- latent external precondition;
- deeper CFG/path propagation bug;
- identity/alias/access-path gap;
- intentional unsupported case.
- Add one minimal regression per class before changing production behavior.
## Concrete next engineering tasks
1. Wire the standalone `GuardedFactStore` into production `SymbolicEnv` fact lookups behind a narrow read/query API, with tests before detector migration.
2. Migrate `div-by-zero` to guarded-fact queries first, then migrate `null-deref` and `oob` after div stabilizes.
3. Refine AccessPath havoc from coarse clear-all to dependency-root invalidation for mutable refs, `&aggregate`, and pointer-reachable calls.
4. Add generic assertion/contract modeling for `assert`, Fold assertion macros, `Expects`, and configured project macros without YAMS-specific suppressions.
5. Promote benchmark baselines into a required maturity gate, including structured optional-tool skips and fail-closed malformed-baseline behavior.
6. Keep rerunning the YAMS focused shard and source/binary focused gates after each production behavior change.
## Sources
- angr docs: symbolic expressions/solver, SimState, machine state, symbolic memory addressing/concretization strategies.
- angr source inspected: `angr/sim_state.py`, `angr/state_plugins/solver.py`, `angr/storage/memory_mixins/address_concretization_mixin.py`.
- KLEE paper: Cadar et al., *KLEE: Unassisted and Automatic Generation of High-Coverage Tests for Complex Systems Programs*, OSDI 2008.
- Clang Static Analyzer source/docs: `ProgramState.h`, `ConstraintManager.h`, checker development manual, IPA docs.
- CBMC source docs/code search: `symex_target`, `symex_target_equation`, `symex_goto`, `ssa_step` guarded SSA/equation model.
- Infer Pulse docs and source snippets: interprocedural memory safety analysis, latent/manifest issues, optimistic unknown-call handling, access-path heap model.
- Existing Fold docs: `docs/internal/symex-fuzz-landscape.md`, `docs/research/sast-dast-symbolic-roadmap.md`, and current source headers in `include/fold/symbolic/encoder.hpp`.