278 lines
17 KiB
Markdown
278 lines
17 KiB
Markdown
# Z3-backed complexity analysis feasibility and SAST lane map
|
|
|
|
> Date: 2026-06-10
|
|
> Scope: research/planning pass for adding complexity/resource analysis to Fold's source code-analysis lane while avoiding duplicated scanner and security/SAST work.
|
|
|
|
## Executive takeaways
|
|
|
|
1. **Feasible, but not as "ask Z3 for Big-O".** Z3 is useful for proving loop bounds, ranking functions, path feasibility, transition implications, and bounded cost obligations. It should sit under a small resource-bound model, not become the whole complexity analyzer.
|
|
2. **Start with security-relevant complexity, not general asymptotic inference.** The most Fold-aligned MVP is a `complexity`/`resource-bound` source detector that reports likely algorithmic-complexity DoS, nested input-dependent loops, unbounded loop/call costs, and complexity regressions under a conservative confidence model.
|
|
3. **Reuse the existing security walk.** Fold already has a composite source security walker and observer hook model. Complexity should either be another observer over that walk for statement/event counting or a sibling walk that consumes the same `SourcePathState`, range facts, `Octagon`, and loop facts. Do **not** create another ad hoc AST traversal with its own path/range model.
|
|
4. **Refactor detector registration first.** Adding another detector by extending the current boolean-heavy `SourceDetectorPlan` and large `scan_runner.cpp` if-chain will increase duplicated work. Move toward descriptor-driven detector registration before or during the complexity MVP.
|
|
5. **Library strategy: borrow ideas, not embed a big tool.** LLVM ScalarEvolution, Infer Cost, KoAT/AProVE/CoFloCo/LoAT, CiaoPP-style Horn/cost relations, CRAB/Clam, and Z3 muZ are the right references. Direct embedding is likely too heavy or mismatched for Fold's Clang-AST lane.
|
|
|
|
## Literature and tool evidence
|
|
|
|
| Source/tool | What it does | Lesson for Fold |
|
|
|---|---|---|
|
|
| **Infer Cost** (`infer --cost`) | Computes asymptotic execution cost and supports complexity diffing/regression reporting. Docs state Java is primary; C/C++ support is limited. It reports degree increases such as `O(n)` -> `O(n^2)` through `reportdiff`. | Fold should separate **cost summaries** from **findings**, and support baseline/regression workflows rather than only per-scan warnings. |
|
|
| **LLVM ScalarEvolution (SCEV)** | Models induction variables, add recurrences, exit/backedge counts, and trip counts in LLVM IR. | Best practical library idea for loop trip-count recognition. Since Fold is AST-first, use SCEV as design vocabulary or optional LLVM-lane later, not as the first dependency. |
|
|
| **KoAT / AProVE** | Automatic complexity/termination for integer programs, including C integer programs via translation, with upper runtime and size bounds. | Mature approach: translate loops/procedures to integer transition systems, infer size bounds and runtime bounds modularly. Good long-term export/backend model. |
|
|
| **CoFloCo / COSTA/PUBS family** | Infers symbolic upper/lower bounds from cost equations independent of source language. Often uses recurrence/cost-relation generation plus solving. | Fold can generate a small internal `CostRelation` IR and only later export to CoFloCo-like formats. |
|
|
| **LoAT** | Lower-bound/nontermination analysis over transition systems and linear CHCs; parses CHC SMT-LIB. | Useful for proving worst-case lower-bound hazards (`at least n^2`) and nontermination candidates, but not MVP. |
|
|
| **Difference-constraint resource bound analysis** | Literature shows scalable imperative-program bounds by modeling transitions like `x' <= y + c`, and mutually inferring transition and variable bounds. | Fold already has intervals/octagons; first MVP should target difference constraints and affine loop updates. |
|
|
| **CHC/Horn resource verification / CiaoPP** | Translates resource usage specifications and inferred resource functions into Horn clauses, often with interval preconditions. Z3's muZ supports CHCs. | Good fit for future `--complexity-chc` experimental backend. Use CHCs for summaries/procedures once intra-procedural loop bounds work. |
|
|
| **Ranking-function synthesis** | Linear ranking functions prove termination and bound simple loop progress using SMT obligations. | Fold can use Z3 to prove candidate ranking/variant functions for loops and turn them into bounded trip counts. |
|
|
| **pymwp** | Static analyzer for C programs that certifies polynomial growth bounds, compositional and automated, but not a Z3-centric security scanner. | Useful alternate target: classify polynomial vs unknown/exponential-ish growth before trying exact Big-O. |
|
|
|
|
Key URLs reviewed:
|
|
|
|
- Infer Cost: https://fbinfer.com/docs/checker-cost/
|
|
- Infer repo/manpage: https://github.com/facebook/infer
|
|
- LLVM SCEV header/docs: https://github.com/llvm/llvm-project/blob/main/llvm/include/llvm/Analysis/ScalarEvolution.h
|
|
- SCEV explainer: https://www.npopov.com/2023/10/03/LLVM-Scalar-evolution.html
|
|
- KoAT: https://koat.verify.rwth-aachen.de/
|
|
- AProVE KoAT C integer programs: https://aprove.informatik.rwth-aachen.de/interface/v-koat/c
|
|
- CoFloCo: https://github.com/aeflores/CoFloCo
|
|
- LoAT: https://loat-developers.github.io/LoAT/
|
|
- Z3 fixedpoints/muZ: https://microsoft.github.io/z3guide/docs/fixedpoints/intro/
|
|
- Difference-constraint resource bounds: https://link.springer.com/article/10.1007/s10817-016-9402-4
|
|
|
|
## Feasibility assessment
|
|
|
|
### What Z3 can do well for Fold
|
|
|
|
- Prove **path feasibility** for loop-entry/exit guards, reusing `SourcePathState`.
|
|
- Prove candidate **linear ranking functions** and monotone progress facts.
|
|
- Prove/refute simple **trip-count upper bounds** such as `0 <= i < n`, `i += c`, `n - i` decreases.
|
|
- Prove **nested-loop multiplication conditions** when inner bounds depend on outer/input variables.
|
|
- Check **threshold obligations**: `cost > limit`, `cost grows with tainted input`, `cost_after > cost_before` for baseline comparison.
|
|
- Validate detector claims under current path/guarded facts, keeping `unknown` as precision loss.
|
|
|
|
### What Z3 should not be expected to do alone
|
|
|
|
- Infer closed-form Big-O for arbitrary C++ control flow, heap containers, iterators, recursion, virtual calls, callbacks, or data-dependent loops.
|
|
- Solve nonlinear recurrences robustly under scanner budgets.
|
|
- Replace abstract interpretation/polyhedral/SCEV-style loop recognition.
|
|
- Produce useful complexity reports without a separate cost model and summary format.
|
|
|
|
### Practical MVP confidence levels
|
|
|
|
| Confidence | Meaning for complexity |
|
|
|---|---|
|
|
| `proved` | Bound/regression/hazard proved under Fold's bounded/inferred model and current path facts. |
|
|
| `latent` | Complexity hazard depends on caller/input size precondition or tainted input range not discharged locally. |
|
|
| `heuristic` | Structural pattern indicates risk, but loop bound/cost is partial or unsupported. |
|
|
| `syntactic` | Pattern only, e.g. nested loops over same container with no bound proof. |
|
|
|
|
A general complexity detector should use the same bounded-proof humility as security detectors: proved within model, not universal program complexity.
|
|
|
|
## Fold scan/SAST lane map
|
|
|
|
### Current source detector selection
|
|
|
|
`SourceDetectorPlan` is a hand-maintained boolean struct: `include/fold/cli/detector_plan.hpp:14-44`. Profiles and aliases are duplicated manually in `src/cli/detector_plan.cpp:45-77` and `src/cli/detector_plan.cpp:85-117`.
|
|
|
|
Implication: adding `complexity` by just appending more booleans will work but increase maintenance duplication. A detector descriptor table should own:
|
|
|
|
```text
|
|
name, aliases, category, backend/lane, profile name, default group, factory/run mode,
|
|
proof mode, summary-backed?, cross-tu?, report kind
|
|
```
|
|
|
|
### Current source security scan path
|
|
|
|
- `src/cli/scan_runner.cpp:1990-1992` runs composite security detectors when enabled.
|
|
- Fallback individual detector paths duplicate boilerplate for each detector, e.g. div/null/int/oob at `src/cli/scan_runner.cpp:1995-2065`.
|
|
- Composite registration happens in `src/cli/scan_runner.cpp:2352-2419`, building observers for div, null, overflow, truncation, sign conversion, OOB, taint, SQL injection, and double-free.
|
|
- `src/cli/scan_runner.cpp:2423-2431` multiplexes observers through one `walkContextWithObserver(...)` call.
|
|
- Results/profile attribution are split back out at `src/cli/scan_runner.cpp:2436-2455`.
|
|
|
|
Implication: complexity analysis should not add another full TU walk if it only needs AST/control-flow facts already seen by `SecurityWalker`. Prefer a new observer or a shared `AnalysisWalker` facade.
|
|
|
|
### Current security walker/state path
|
|
|
|
- `BatchSafetyObserver` queues hook calls and flushes under cooperative deadline: `src/detect/security_walker.cpp:1225-1269`.
|
|
- `SecurityWalker` owns per-function CFG/path traversal and budget checks: `src/detect/security_walker.cpp:1496-1525`.
|
|
- Per-function setup creates `SymbolicEnv`, Z3 solver, def-use, initial contracts, and then walks CFG/body: `src/detect/security_walker.cpp:2853-2888`.
|
|
- Source path API exists: `include/fold/symbolic/source_path_state.hpp:19-47`.
|
|
- Existing loop/invariant hooks are minimal but relevant: `include/fold/symbolic/loop_invariant.hpp:10-23`.
|
|
- Existing octagon relational facts can support difference constraints: `include/fold/symbolic/octagon.hpp:15-32`.
|
|
|
|
Implication: complexity should reuse this function setup and enrich it with a `LoopCostAnalyzer` rather than maintain separate path, contract, debug-assert, and def-use logic.
|
|
|
|
### Current reporting model
|
|
|
|
`FindingKind` is security/quality oriented and has no complexity/resource kind today: `include/fold/report/finding.hpp:13-72`. Evidence already has backend/proof/path/precondition fields that complexity can reuse: `include/fold/report/finding.hpp:177-214`.
|
|
|
|
Recommendation: add one or two finding kinds only after the design is stable:
|
|
|
|
- `ResourceComplexity` / `AlgorithmicComplexity` for security-relevant input-driven cost hazards.
|
|
- Optionally `ComplexityRegression` for baseline diff mode, if reports should show it as a finding rather than a profile artifact.
|
|
|
|
For an MVP, a JSON sidecar (`complexity.summaries.json`) may be safer than immediately expanding SARIF rules.
|
|
|
|
## Where we may be doing double work
|
|
|
|
1. **Detector registration/orchestration duplication.** `SourceDetectorPlan` + profile list + alias enablement + scan-runner if-chains are all manually synchronized. Complexity will worsen this unless registration becomes descriptor-driven.
|
|
2. **Multiple path/range models.** SecurityWalker, individual detectors, dead/redundant/uninit walkers, intervals, octagon, guarded facts, and loop invariant hooks overlap. Complexity should consume a single `FunctionAnalysisFacts` product rather than clone range/path logic.
|
|
3. **Separate structural detectors vs observer detectors.** Some detectors run on the composite observer path; others run custom AST/CFG traversals. A complexity pass will need both structural loops and symbolic facts. This argues for a shared `SourceAnalysisContext` with path facts, loop facts, def-use, summaries, and budget.
|
|
4. **Profiles vs findings.** Fold already has rich solver/profile counters in `SolverStats`; complexity may be both a metric and a finding. Keep summary/profile output separate from security findings until the user asks for gating/security warnings.
|
|
5. **Source vs binary risk.** Do not route binary P-Code into source complexity IR. Binary can later have separate heuristic cost summaries, but source complexity should remain Clang/AST/CFG grounded.
|
|
|
|
## Recommended architecture
|
|
|
|
### New source-lane concepts
|
|
|
|
```cpp
|
|
struct CostSymbol {
|
|
std::string name; // e.g. n, size(v), len(s), argc
|
|
ir::AccessPath subject; // canonical source identity where available
|
|
};
|
|
|
|
struct LoopBound {
|
|
enum class Kind { Constant, Linear, Polynomial, Unknown, UnboundedCandidate };
|
|
Kind kind;
|
|
std::string expression; // textual symbolic bound: n, n*m, n^2
|
|
z3::expr guard; // path under which bound applies
|
|
Confidence confidence;
|
|
};
|
|
|
|
struct CostSummary {
|
|
std::string function;
|
|
std::string cost_expr; // O(1), O(n), O(n*m), unknown
|
|
std::vector<LoopBound> loops;
|
|
EncodingPrecisionContext precision;
|
|
};
|
|
```
|
|
|
|
### Analysis pipeline
|
|
|
|
```text
|
|
Clang AST/CFG
|
|
-> existing SecurityWalker/SourcePathState setup
|
|
-> LoopCostCollector: loop headers, guards, induction vars, call costs
|
|
-> Bound inference:
|
|
1. cheap syntactic affine recognizer
|
|
2. interval/octagon/difference-constraint facts
|
|
3. Z3 candidate ranking/bound proof
|
|
4. unknown/timeout precision tag
|
|
-> CostSummary sidecar
|
|
-> optional Finding when security-relevant or regression-threshold crossed
|
|
```
|
|
|
|
### Detector shape
|
|
|
|
Do **not** put complexity fully inside `SafetyObserver` if it needs loop headers before expression encoding. Instead:
|
|
|
|
- Add a `SourceAnalysisObserver` or `FunctionAnalysisPass` interface.
|
|
- Keep existing `SafetyObserver` as hazard-sink observer.
|
|
- Let both share `SourceAnalysisContext`:
|
|
|
|
```cpp
|
|
struct SourceAnalysisContext {
|
|
clang::ASTContext& ast;
|
|
const clang::FunctionDecl* function;
|
|
SymbolicEnv& env;
|
|
SourcePathState& state;
|
|
const ir::DefUseChains& def_use;
|
|
const symbolic::Octagon& octagon;
|
|
SolverBudget budget;
|
|
SolverStats& stats;
|
|
};
|
|
```
|
|
|
|
This is the refactor that prevents complexity from redoing contract setup, path pruning, range facts, and solver accounting.
|
|
|
|
## MVP roadmap
|
|
|
|
### Phase 0 — design and dedup before features
|
|
|
|
- Add a detector descriptor table to replace parallel bool/profile/name wiring for source detectors.
|
|
- Add `complexity` as a recognized detector flag but keep it hidden/experimental.
|
|
- Add `CostSummary` sidecar schema behind `--profile` or `--complexity-output`.
|
|
|
|
Validation: detector-plan unit tests should prove every descriptor has one profile/rule alias and every CLI alias maps exactly once.
|
|
|
|
### Phase 1 — cheap loop inventory and cost sidecar
|
|
|
|
- Build `LoopCostCollector` over Clang AST/CFG.
|
|
- Emit per-function loop count, nesting depth, tainted/input-like variables used in loop guards, and unknown-call-in-loop markers.
|
|
- No findings yet unless `--complexity-report-heuristic` is set.
|
|
|
|
Validation fixtures:
|
|
|
|
- constant loop -> `O(1)` / bounded
|
|
- `for i < n` -> linear symbolic bound
|
|
- nested `for i<n; for j<n` -> `n^2` structural estimate
|
|
- loop over `argv`/`read`/`size()` input -> latent security relevance
|
|
|
|
### Phase 2 — Z3-proved affine loop bounds
|
|
|
|
- Recognize induction var init/update/guard.
|
|
- Generate candidate bound obligations and prove with Z3 under `SourcePathState` path facts.
|
|
- Reuse `Octagon` for difference constraints and `GuardedFactStore` for guarded range facts.
|
|
|
|
Examples:
|
|
|
|
- `for (int i = 0; i < n; ++i)` proves `<= max(n, 0)`.
|
|
- `while (i < n) { i += 2; }` proves linear with step.
|
|
- Early-return guards constrain latent preconditions, e.g. `if (n > 1000) return;` caps cost.
|
|
|
|
### Phase 3 — security/SAST findings
|
|
|
|
Add `AlgorithmicComplexity` finding only for actionable hazards:
|
|
|
|
- user-controlled/input-sized loop cost exceeds configured degree/threshold;
|
|
- nested input-dependent loops in request/parse/crypto/string paths;
|
|
- unbounded loop with tainted exit condition;
|
|
- complexity regression vs baseline.
|
|
|
|
Keep general summaries as sidecar/profile data, not warnings.
|
|
|
|
### Phase 4 — interprocedural cost summaries
|
|
|
|
- Add function `CostSummary` to existing summary store or a sibling cache.
|
|
- Compose call cost inside loops.
|
|
- Treat unknown calls with a configurable cost policy (`O(1)`, unknown, summary-only).
|
|
|
|
### Phase 5 — optional external backend experiments
|
|
|
|
- Export integer transition systems or cost relations for KoAT/CoFloCo/LoAT experiments.
|
|
- Try Z3 muZ CHCs for loop/procedure summaries.
|
|
- Consider optional LLVM/SCEV lane only if AST approach hits clear precision limits.
|
|
|
|
## Security/SAST mapping
|
|
|
|
Complexity analysis should be in the **source SAST lane** initially:
|
|
|
|
- It is source semantics sensitive: C++ containers, functions, input APIs, contracts.
|
|
- It reuses Clang AST/CFG and existing symbolic facts.
|
|
- It can produce security findings when attacker-controlled inputs drive high cost.
|
|
|
|
It should **not** be in binary mode initially:
|
|
|
|
- Binary loop bounds from P-Code need separate register/memory/CFG recovery.
|
|
- Binary cost evidence would be heuristic and should stay separate if added later.
|
|
|
|
It should **integrate with confirmation later**:
|
|
|
|
- Fuzz/DAST can measure slow inputs/timeouts, but runtime timing is noisy.
|
|
- Confirmation task schema may need `hazard=algorithmic-complexity`, `resource=time|allocations`, `expected_violation=cost threshold exceeded`.
|
|
|
|
## Recommended next tasks
|
|
|
|
1. Refactor source detector registration to descriptor-driven metadata.
|
|
2. Add `CostSummary` model and sidecar serialization tests.
|
|
3. Implement loop inventory collector with no findings.
|
|
4. Add affine Z3-bound proof for simple `for`/`while` loops.
|
|
5. Add security-relevance classification using taint/input-source and request/parse/string sink context.
|
|
6. Add benchmark fixtures and baseline/regression gate.
|
|
|
|
## Non-goals for the first implementation
|
|
|
|
- Full arbitrary C++ Big-O inference.
|
|
- Whole-program CHC engine.
|
|
- Binary P-Code complexity proof.
|
|
- Exact container/allocator modeling.
|
|
- Replacing existing security detectors with complexity analysis.
|