fold/docs/research/assertion-invariant-maturity-audit-2026-06-03.md
2026-06-04 20:20:29 -04:00

187 lines
7.6 KiB
Markdown

# Fold Assertion and Invariant Maturity Audit
> Date: 2026-06-03
> Scope: assertion policy and invariant hardening for Fold's source, binary,
> frontend, report, and confirmation paths.
## Current assertion model
Fold does not currently have a project-local assertion layer such as
`FOLD_ASSERT`, `FOLD_DCHECK`, or `FOLD_UNREACHABLE`.
Current correctness controls are mostly:
- ordinary error returns (`std::nullopt`, `{}`, error strings);
- warning/error logging via `spdlog`;
- tests and golden fixtures;
- report-level filtering/deduplication;
- graceful optional-tool fallback for r2 CLI, KLEE, Swift/Rust frontends, and Ghidra specs.
This is appropriate for environmental/recoverable failures, but several internal
analysis invariants should be made explicit over time so bugs fail near the
cause instead of leaking misleading findings.
## Policy recommendation
Add a small project-local assertion layer only after one or two high-value call
sites prove the need. Do not migrate broadly in one sweep.
Suggested API:
```cpp
FOLD_ASSERT(cond, "message") // always-on invariant; terminates
FOLD_DCHECK(cond, "message") // debug/canary-only sanity check
FOLD_PRECONDITION(cond, "message") // programmer contract at API boundary
FOLD_UNREACHABLE("message") // impossible control-flow path
```
Initial implementation should include file/line/function diagnostics, preferably
through `std::source_location` where the project C++ standard permits it, or a
small macro wrapper if not.
Do not use assertions for:
- missing optional tools;
- missing compile DB in a mode where the user can fix input;
- unsupported binary architecture;
- malformed external frontend output;
- missing KLEE/SymCC/angr adapters.
Those remain recoverable errors or skips.
## Highest-value invariant targets
| Priority | Site/flow | Recommended tier | Invariant |
|---:|---|---|---|
| 1 | Report emission / benchmark comparator | Always-on or test gate | Emitted user-facing findings have physical locations and backend provenance. |
| 1 | `scan_router` mode resolution | Always-on internal check | `source`, `binary`, `hybrid`, `auto` resolve to the intended backend set; hybrid does not silently erase source findings. |
| 1 | P-Code SSA call annotation | Debug + unit-gated invariant | Raw call-site names are not overwritten by synthesized SSA target fallback. |
| 1 | P-Code raw div/rem gating | Debug + unit-gated invariant | If machine division addresses are known, div/rem findings only originate at those addresses. |
| 2 | Rust/Swift frontend NDJSON parsing | Recoverable error + counter metric | Malformed nodes degrade per-node or per-function; they must not abort the whole scan. |
| 2 | Confirmation task/result JSON | Always-on parser validation | Required schema keys are present; unknown optional keys are tolerated; canonical names remain centralized. |
| 2 | Finding construction helpers | Always-on invariant in helper | New findings set detector name, backend, confidence, and at least one location unless explicitly internal. |
| 3 | Benchmark harness baselines | Always-on script failure | Malformed spec/baseline files fail closed, not as a passing empty suite. |
| 3 | Cache/fact identity | Debug check | Fact IDs and summary cache keys are stable and non-empty for stored entries. |
## Gap list
### Missing always-on invariant checks
- No central finding builder enforces location/backend/detector completeness.
- The report filter drops locationless findings, but the producing site may not
fail or count the invariant violation.
- Router mode/backend resolution is tested, but not asserted at the transition
where findings are merged.
- Confirmation task/result schemas round-trip in tests, but parser failures are
not yet framed as schema invariants with clear diagnostics.
### Debug-only checks worth adding first
- P-Code call annotation should assert in debug/canary that an existing
call-site-resolved `call_target` is not replaced by a different synthesized
target.
- Raw P-Code div/rem findings should log/debug-count cases suppressed because the
originating machine instruction is not a real division.
- Rust/Swift IR encoders should count per-node degradation events so a frontend
regression is visible even when the scan succeeds.
### Avoid assertion misuse
- Do not assert when optional tools are absent. `r_core`, Capstone, KLEE, Swift,
and Rust frontends all have legitimate unavailable states.
- Do not assert on user/project input errors such as missing paths or invalid
compile databases. Return exit code 2 with actionable diagnostics.
- Do not assert inside benchmark comparators for expected benchmark failures;
return structured JSON with failures.
## Rollout plan
### Step 1 — codify invariants in benchmark gates
Already started:
- `compare_findings.py` fails on missing physical locations;
- `compare_findings.py` fails on missing backend provenance;
- binary/source golden specs express `must_report` and `must_not_report` rules.
Next:
- add a benchmark summary field for suppressed/degraded encoder events;
- fail benchmark specs with zero tests unless explicitly allowed.
### Step 2 — add central finding validation helper
Create a small helper in report or scan code, for example:
```cpp
bool isUserFacingFindingWellFormed(const Finding& f);
```
Use it in:
- report filtering tests;
- benchmark comparator equivalent in C++ tests;
- optional debug logging before report emission.
Only after that consider always-on `FOLD_ASSERT` in narrow internal helpers.
### Step 3 — add assertion header if repeated call sites justify it
Potential file:
```text
include/fold/core/assert.hpp
```
Initial macros/functions:
- `FOLD_ASSERT`
- `FOLD_DCHECK`
- `FOLD_UNREACHABLE`
Keep it independent of detector code and avoid side-effectful expressions.
### Step 4 — migrate high-risk sites only
First candidates:
1. P-Code call-target annotation conflict check.
2. Finding-builder completeness checks in shared helper paths.
3. Router merge provenance checks.
4. Confirmation schema parse/serialize invariants.
Do not migrate every `return {}` or `std::nullopt`; many are intentional
recoverable degradation paths.
## Concrete candidate sites
| File | Candidate | Why |
|---|---|---|
| `src/binary/pcode_router.cpp` | `annotateBySite` / SSA annotation | Prevent call target confusion that caused `memchr`/`memcpy` FPs. |
| `src/symbolic/pcode_encoder.cpp` | div/rem hazard gate | Ensure known machine-address gating remains active for raw scans. |
| `src/report/filter.cpp` | post-filter sanity | Detect whether locationless findings are being produced frequently. |
| `src/cli/scan_router.cpp` | backend merge | Ensure router-stamped provenance survives merge. |
| `src/confirm/confirmation_task.cpp` | JSON parsing | Enforce canonical keys and required fields. |
| `src/frontend/rust_backend.cpp` / `src/frontend/swift_backend.cpp` | NDJSON parse degradation counters | Make frontend artifact regressions visible without crashing scans. |
| `scripts/benchmarks/compare_findings.py` | spec shape validation | Fail closed on malformed benchmark specs. |
## Suggested validation for assertion rollout
Before adding an assertion macro:
```sh
meson test -C build/release fold:pcode_encoder fold:pcode_summary fold:scan_e2e --print-errorlogs
scripts/benchmarks/summarize_maturity.py --build build/release --all
```
After adding always-on assertions, also run:
```sh
meson test -C build/release --print-errorlogs
```
## Current recommendation
Do not add a broad assertion layer yet. The immediate maturity win is to keep
encoding invariants as benchmark gates and focused tests. Add `FOLD_ASSERT` only
when a second or third high-value C++ call site needs the same invariant behavior.