7.6 KiB
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:
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_targetis 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.pyfails on missing physical locations;compare_findings.pyfails on missing backend provenance;- binary/source golden specs express
must_reportandmust_not_reportrules.
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:
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:
include/fold/core/assert.hpp
Initial macros/functions:
FOLD_ASSERTFOLD_DCHECKFOLD_UNREACHABLE
Keep it independent of detector code and avoid side-effectful expressions.
Step 4 — migrate high-risk sites only
First candidates:
- P-Code call-target annotation conflict check.
- Finding-builder completeness checks in shared helper paths.
- Router merge provenance checks.
- 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:
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:
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.