Problem: a TU skipped because a prior run quarantined it was not analysed,
but coverage did not say so. The serial path dropped it from the attempted
set, so a scan whose only TU was quarantined was complete and exited 0; the
fan-out path saw no file_finished record and called it frontend-failed.
Fix: the shard runner counts every requested TU and records skipped ones
with gap reason `quarantined`; a worker emits a file_finished record with
that reason so the parent classifies it the same way. USAGE lists the reason.
Test: test_subprocess_fanout "[quarantine]" covers serial (was 2 attempted,
no gap) and fan-out (was frontend-failed). CLI probe: serial and fan-out
both exit 2 with a `quarantined` gap.
The README listed 11 of fold's 17 detectors, never explained the
confidence tiers every finding carries, did not mention the new exit
code 2 / coverage reporting, and spent half its length on a
performance roadmap.
It now says what fold is, defines the five tiers (from
include/fold/report/finding.hpp, including concrete-witness and
latent), what "bounded" means after the loop-exit fix, a quick start,
output formats and exit codes (0 none, 1 any unsuppressed finding, 2
unwritable report or any unanalysed translation unit), and links. The
detector list defers to the complete table in docs/USAGE.md.
The performance section moves to docs/architecture/performance.md, with
one correction: the gold set was derived from fold's own reviewed output
and is matched by detector kind and file, so it catches drift rather
than measuring precision/recall independently. The lint pre-commit hook
note moves to BUILD.md.
Problem: docs/positioning.md claimed four confidence tiers (there are
five, including Latent) and defined Proved as any Z3 SAT, which
contradicts soundness.md's exact-model requirement. It claimed "fold
scans a 100k-LOC codebase in minutes" with no committed benchmark (the
audit measured 2 files in 5.9 s at 407 MB RSS). It described CBMC as
asking Z3/MathSAT (its default back end is SAT) and as scaling poorly
past a few thousand lines, without support. It claimed a scan-to-fuzz
loop "no other open-source analyzer covers" and "no commercial vendor
ships" (directed and hybrid fuzzers such as AFLGo and SAVIOR cover
related ground, and prior-art.md cites that line of work). It said most
analyzers "stop at SAT", ignoring the Clang Static Analyzer's Z3
refutation.
Fix: five tiers with Latent defined; Proved requires an exact bounded
model; the CBMC comparison describes the method and fold's real scope
(per-sink, mostly intraprocedural, unroll 5) instead of speed; the
uniqueness claims now describe the loop without exclusivity; CSA's Z3
refutation is acknowledged. The README is left for a separate docs pass.
Problem: the README said setup enables "tracked ... OpenGrep pre-push
hooks", but only .githooks/pre-commit is tracked, so fresh clones get no
pre-push hook. docs/research/prior-art.md pointed to prior-art-full.md,
which has never been in the repository. test_mcp_stdio's TempDir
destructor called the throwing remove_all overload; a failure during
unwinding terminates the test binary and leaves .fold-mcp-test-*
directories behind.
Fix: the README names only the tracked pre-commit hook. The pre-push
hook is not committed: it runs `opengrep --config auto`, which needs the
network. The dead reference is removed. TempDir uses the error_code
remove_all overload. It stays under the working directory, because the
MCP server rejects paths outside its workspace root (moving it to
temp_directory_path broke three MCP tests).
Test: mcp_stdio passes.
Problem: a TU the front end could not parse left no trace. ClangTool::run's
status was discarded, TUs with compile errors were analyzed on a partial
AST and counted as done, and the isolated-worker path dropped TUs that
never produced an AST. A scan of only broken files printed no findings and
exited 0, the same as a clean scan. Separately, the scan logger wrote to
stdout, so any warning corrupted a SARIF/JSON report written to stdout.
Fix: every TU now yields an outcome (clean, compile-errors,
analysis-failed); workers carry it on their file_finished record, and the
fan-out parent adds timeout/crash/worker-exit for the TU a worker died on.
tuCoverage() turns the requested file list plus outcomes into a
ScanCoverage (requested TUs with no outcome are frontend-failed). SARIF
gets a run `invocations` entry (executionSuccessful, one
toolExecutionNotification per gap); findings JSON gets an additive
`coverage` object. A scan with any gap, or with nothing analyzed, exits 2
after writing its report. CLI diagnostics now go to stderr, as
docs/internal/cli-philosophy.md principle 12 requires. USAGE.md and
AGENTS.md document the coverage fields and the exit code.
Test: tests/unit/test_scan_coverage.cpp (classification, path spelling,
complete/untracked controls, merge, both writers, and unchanged output
without coverage); test_cli_paths "[coverage]" covers a clean TU (exit 0,
complete) and a TU with a missing include (exit 2, gap in JSON, SARIF on
stdout parseable with executionSuccessful=false). Before: both probe files
exited 0 with no coverage information. Full suite: 139/139.
Measured (scratch, reverted): with the graph-fed cross-TU candidate
expansion disabled, finding fingerprints stay exact (fold 0fec78e5...,
yams 247523c5..., status ok) and securityGraphCandidateFunctions
returns an empty set (expanded=0) for these manifests. The 18.8s graph
build is pure overhead for the expansion path here. Remaining graph
consumer is summary priming; S0b tests whether its SCC-ordered eager
prepass can be dropped in favor of the lazy on-demand summary path.
README 'Performance architecture' documents the measurement-first,
gate-guarded optimization loop and the roadmap decision: collapse the
double per-TU parse into single-pass graph building over PCH/module
caching, chosen specifically to preserve portability. The decision
doc records the option-2 scoping: the graph->detector dependency map
(candidate expansion, summary priming, cross-TU property priming) and
the open memory-vs-parse tension to resolve before implementation.
Iteration 12 of the fold optimization loop
(docs/research/optimization-loop-2026-07-30.md).
Implementation of the recorded design began (~7 files plumbed) and
collapsed on an ordering dependency: assembleProjectCallGraph runs
BEFORE the main TU pass because the TU pass consumes the graph
(graph_security_candidates cross-TU expansion; summary priming from
the call graph). In-pass collection cannot serve the same scan —
the TU pass needs the graph the contributions would feed. Reversing
the order breaks candidate expansion (finding fingerprints would
change), and keeping every TU AST alive across both passes is
infeasible (~1.4 GiB RSS for the fold self-scan).
All plumbing reverted; production byte-identical. The ~16.9s
no-index exhaustive build is the inherent cold-run price of
cross-TU facts; target closed.
Iteration 11 of the fold optimization loop
(docs/research/optimization-loop-2026-07-30.md).
Records the campaign iteration ledger (10 iterations, fold self-scan
wall 251.7s -> 86.2s, 66% reduction, fingerprints exact throughout)
and the completed design for the deferred in-pass graph contribution
collection (the no-index exhaustive call-graph build's ~16.9s
re-parse): expose buildContributionFromContext, accumulate
contributions in the main TU pass, feed prebuilt contributions to
assembleProjectCallGraph when they cover the shard's files and no
project index is open. Equivalence argument and worst-case fallback
preservation documented; implementation is the next slice.
No production code change.
Iteration 1 of the fold optimization loop
(docs/research/optimization-loop-2026-07-30.md, workflow-driven
measure->select->implement->validate->checkpoint).
Baseline measured (tests/benchmarks/runs/optimization-2026-07-30/):
- fold self-scan median 251.7s, peak RSS 1390.5 MiB; sql-injection
detector 40.37% of analyze share (101.4s median) with 0 Z3 queries
- yams self-scan median 10.7s, peak RSS 489.9 MiB
Implement attempt: shared thread-local scratch Z3 context for
summarizeLeafFunctionSymbolically (stack samples showed Z3 ast_table
init from the ensureSymbolicSummary chain dominating walk time).
REJECTED by the new characterization test: Z3 hash-consing collapses
same-named parameters across functions and summary composition
silently degrades. Reverted; regression test retained plus rationale
comment at the reverted site.
Also documents: SummaryStore::lookupOrCompute returns pointers into
its internal map (invalidated by clear()); open finding on
load-sensitive summary recomputation under CPU saturation.
No production behavior change; fold:symbolic_summary 10/10 green,
focused suites green, git diff --check clean.
- skills/fold-sast: rename to skill-fold-sast and expand the scan/precision
guidance for C++/Rust/Swift lanes
- docs/architecture: record call-graph phase decomposition and the
optimization-loop TP/FP/TN/FN evidence policy
- docs/mcp-tools + README: update budget/per-query defaults (1800s/6000ms)
and note the OpenGrep pre-push hook
Foundation for replacing fold's dead/abstract binary encoder with a real
SSA P-Code frontend. Verified root-cause gap: serializeBinaryIR() emits only
data-discovery nodes (sections/strings/imports/exports); the control-flow
BinaryExprKind values (BasicBlock/CallSite/...) are never constructed, so the
Z3-BB encoder's encodable filter is always empty and encodeBinaryBBs never
runs — hence 0 binary Z3 findings. The P-Code pipeline supersedes that path.
Phase 0 — docs/internal/ghidra-pcode-integration-research.md: P-Code opcode
core, the decompiler C++ embedding API (LoadImage/SleighArchitecture/Funcdata),
the CORE+DECCORE+SLEIGH source sets, no-bison finding, and pypcode as build
reference.
Phase 1 — vendored Ghidra decompiler at pinned commit (Ghidra_12.1_build,
7e89d94e34...) under third_party/ghidra-decompiler/ (cpp + AArch64 specs +
Apache-2.0 LICENSE + VENDOR.md + .gitignore for build artifacts). Validated by
hand on this clang-21/macOS toolchain:
- full SSA engine (CORE+DECCORE+SLEIGH, 80 files incl. heritage/funcdata/
action) compiles clean, -std=c++11 -O2, NO BFD, NO bison (generated
parsers are committed upstream; ruleparse isn't in the lib set);
- sleigh compiler builds; AArch64 .slaspec -> .sla compiles;
- tools/pcode_dump.cpp (custom LoadImage, no BFD) lifts the real arm64
sym._div_by_zero_direct to correct P-Code: `mov w9,#0 -> COPY(const 0)`,
`sdiv -> INT_SDIV(w8,w9)` — divisor zero-ness explicit in the data flow,
so the Phase-3 encoder proves div-by-zero trivially, multi-arch.
Not yet meson-wired (next step) and no fold-side IR/encoder yet (Phase 2-3);
this commit is the validated vendor + smoke tool.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Record that rust_ir_encoder and binary_bb_encoder do not route through
SecurityWalker/unrollLoop and have no loop handling, so "cross-frontend
ir::Loop reuse" is gated on a shared clang-free loop-walk core rather than
being a standalone slice. Lay out the realistic sequencing (clang-free CFG
foundation -> rust/binary lowering -> recall-preserving while(1) acceleration)
and note no safe small gate-validatable slice exists short of starting the
foundation. Clang counted+affine summarization is complete.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The old fp-baseline.json lived under gitignored bench/results/, so it had
no history and nothing compared against it — precision drift went unnoticed.
- Move the committed baseline to a tracked path (bench/fp-baseline.json)
regenerated from the current build.
- Add `score_fp.py --gate`: diff per-detector P/R/F1 vs the baseline and
exit non-zero on any drop beyond --tolerance (default 0.02).
- Add `--max-files-per-cwe` deterministic stride sampling so huge CWEs are
tractable; restrict gold to the scanned subset so recall stays meaningful.
- Document the gate (local, not CI) in bench/README.md.
- docs/internal/loop-summarization-research.md: Workstream B research + the
null-deref drift root-cause appendix.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Each R2Session construction runs full auto-analysis
- 4-6 aaa runs on a single binary → 60-90s timeout
- Fix: share one R2Session across all extraction calls
- Progressive analysis levels: aa (quick), aaa (full), aaaa (experimental)
- SafetyObserver::seedInterproceduralContext() called before function walks
- OOBObserver override scans all call sites for ConstantArrayType args
- Seeds param_buffer_sizes_ from any caller, regardless of decl order
- Research doc covers Inferbo/ARCHER/FunFrog approaches and fold's infrastructure
- Only 5/15 functions tested; extractCalls/extractXrefs/pdbj untested
- CTF failures mapped to specific r2_backend.cpp code paths
- Top fix: flag dangerous imports + hardcoded strings (S effort, +12 findings)
- Full basic block analysis needed for div-by-zero/null-deref (L effort, +6)
- Z3 solver <0.01% of wall time (74 queries, ~0ms)
- Real bottleneck: AST walkers + CFG traversal (~85%)
- Top optimization: TU pre-filter (skip files with no security ops)
- Bitwuzla multi-solver would not help — SMT queries are already fast
- Bitwuzla BV solver is 5.1x faster than Z3 (SMT-COMP 2023)
- ESBMC pattern: dispatch to 6 solvers, race for first result
- Proposal: add --solver bitwuzla | z3 | portfolio-all flag
- Expected 2-5x speedup on fold's BV-64 queries