Commit graph

372 commits

Author SHA1 Message Date
trvon
5acc896eef
docs(matrix): mark C3 unscheduled -- sized wrong, no forcing function
clang ships the dataflow framework but NO ready-made pointer-
nullability analyzer. Bundled UncheckedOptionalAccessModel is for
std::optional, not raw pointers. C3 would require a custom
DataflowAnalysis<NullPtrLattice> with transfer functions for every
pointer-touching expression -- L, not M as originally sized.

Original sizing column referenced functions_prechecked_safe but
that counter measures syntactic sink-absence, not nullability.

fold already has multiple condition-based non-null pre-passes
(enclosingThenBranchProvesNonNull, ternaryArmProvesNonNull,
priorFunctionTextProvesNonNull, staticParamContractProvesNonNull,
current_debugassert_nonnull_). Reconsider when a concrete null-
deref FP catalog says clang dataflow would catch what these miss.
2026-05-26 09:15:18 -04:00
trvon
55e1d53a9c
docs(matrix): mark C1 unscheduled -- no forcing function
Surveyed sites: dead_branch.cpp has 7 dyn_cast uses, all
single-line type-tag dispatch. security_walker.cpp has 48 of the
same shape. ASTMatchers would be MORE verbose for type-tag
dispatch (MatchFinder + bind + callback structure).

They win for pattern-structure queries (regex name + nth arg
shape + ...), which fold's existing detectors don't need.
"Apply selectively, not bulk" was correct; on inspection there's
no concrete bulk-replaceable site that benefits.
2026-05-26 09:13:48 -04:00
trvon
63e519fb8a
docs(matrix): mark Z9 unscheduled -- infrastructure absent
z3 4.13+ (fold's pinned version) does not export interpolant
APIs; they were a 4.5-era research feature that was removed.
Z3_get_interpolant / mk_interpolant aren't in the linked headers.

Even if available, existing buildCounterexample already emits
concrete model values like `(e.g., when ptr=0)` which are easier
to read than formula slices for the UX-upgrade claim.
2026-05-26 09:12:52 -04:00
trvon
644a6b6b2d
docs(matrix): link Z4 curl baseline JSON
Captures curl scan profile after Z4 (lambda memcpy) lands:
analyze_ms=565020, null-deref wall_ms=344675 / 2,533 queries.

Established convention: each shipped row's Status column links
to its baseline JSON under docs/perf-audit/runs/matrix/.
2026-05-26 09:12:10 -04:00
trvon
da7e072028
perf(symbolic): model memcpy/memmove via z3::lambda (Z4)
Replaces the per-byte `store(store(...))` chain in modelMemcpyLikeCall
with a single `z3::lambda(i, ite(in_range, src_byte, orig_byte))`.
Drops the `!arg_values[2].is_numeral()` bail so symbolic copy_bytes
now reaches the solver instead of being silently skipped.

Concrete cap (kMaxModeledCopyBytes=4096) is kept to prevent
pathological bit-blast explosion on giant memcpy()s. strcpy is
unchanged -- literal bytes are short + concrete and the lambda
form doesn't help there.

z3 simplifies the lambda away at concrete-index selects so QF_BV
stays viable; no logic-change cascade. Full unit suite stays
57/57 green.
2026-05-26 08:47:20 -04:00
trvon
0f462e5b73
docs(matrix): mark C10 unscheduled -- architectural-mismatch FP
clang::CrossTranslationUnitContext is a callee-AST importer via
ASTImporter; fold's CrossTuCache is a property-summary cache.
Different paradigms.

Clang CTU exists for analyses that walk callee statements (CSA-
style). fold deliberately avoids that -- summary_encoder serialises
bodies to SMT-LIB strings, detectors consume properties not AST.
Adopting clang CTU would mean redesigning the analysis paradigm,
not extending it.

The matrix's "selective adoption" framing implied a real selective
use case; on inspection there isn't one in fold's design.
2026-05-25 23:11:59 -04:00
trvon
a791230552
feat(report): C9 FixIts + unified-diff --fix-output
Adds the FixIt collector + per-file unified-diff patch writer
behind a new `fold scan --fix-output <path>` flag. Patch-only --
no in-place rewrites -- so a bad suggestion can't silently
overwrite source.

Consumers (only when the transform is provably sound):
- UninitLocalRead: scalar integer locals only.
  `int x;` -> `int x = 0;`. Non-scalar types deferred.
- DeprecatedApi: regex extracts `[Uu]se foo()` from the
  deprecation message; silent skip when unparseable rather
  than hallucinating a rewrite.

Patches include 3 lines of context above + below each fix so
`git apply --check` accepts them. End-to-end verified against a
synthetic fixture.
2026-05-25 16:40:43 -04:00
trvon
d6e76f45ab
docs(matrix): mark C6 unscheduled -- current-state FP
dead_statement.cpp:140 already filters macro-ID anchors
(battle-tested in 2026-05-15 curl audit: doh.c 9,060 -> 0
findings). Remaining FPs are CFG-construction artifacts
(range-for desugaring, EH stubs), not preprocessor-induced.

fold has no multi-config scan mode, so "variant -D" was a
speculative failure mode. If a real dead-branch macro FP surfaces,
the fix is a one-line isMacroID() filter on dead_branch.cpp, not
a Preprocessor-hook row.
2026-05-25 12:16:48 -04:00
trvon
48dac13d53
feat(detect): uninit-local-read detector (C5 wiring)
Graduates C5's UninitVariablesAnalysis wrapper from
measurement-only into a real detector. Emits a finding per read of
an uninitialised local in the TU's main-file functions.

Scope is calibrated (C5's wrong-hypothesis pivot): clang's analyzer
flags LOCAL read-before-write, NOT parameter nullability. Tests
pin this explicitly (`int p; return p; -> no finding`).

End-to-end on synthetic fixture: 3 expected findings (read of
uninit local, self-init `int x = x`, subsequent use of the
self-init). Parameter reads correctly skipped.

scan_runner.cpp dispatch is unstaged in this commit -- parallel
session has unrelated in-flight edits to that file. The detector
is registered and tested; scan-runner wiring lands in a follow-up.
2026-05-25 11:52:20 -04:00
trvon
95b1ae777b
docs(matrix): mark Z6 unscheduled -- infrastructure FP
`inferCountedLoopInvariant` already runs in security_walker, but
its result only feeds spdlog::debug -- `refineRange` and
`recordRangeInOctagon` run unconditionally regardless of invariant
proof.

Extending fixedpoint to mutual recursion before wiring the existing
single-loop invariant into analysis state would add capability
nothing reads. The actual fix is precision-bug-shaped (consume the
result), not library-expansion-shaped.
2026-05-25 11:25:41 -04:00
trvon
d410bb7ba7
feat(fuzz): refine SAT models for smaller integer witnesses (Z5)
When a witness recorder is active, run z3::optimize.minimize on the
SAT model before extracting bytes for fuzz seeds. Refines integer
params toward the smallest path-and-hazard-satisfying value, so
seeds emit `arg=1` instead of `arg=0xDEADBEEF`.

Measured on synthetic access_at_offset(p, offset) with the path
`0 < offset && offset < 100`: primary-witness median 33 -> 5
(85% smaller). divide_by_count(p, count) with `count > 0` was
already path-minimal so no change.

Only the primary witness is refined; recordAlternateWitnesses
still uses the original model so the corpus covers the
equivalence class.

Curl-scale validation deferred -- fuzz-prep on full curl crashes
pre-existing (verified Z5-OFF baseline exit 139).
2026-05-25 11:22:38 -04:00
trvon
071f469d1d
docs(matrix): batch audit -- Z3 FP, Z4 site correction
Z3 ("byte-unroll elimination") was wrong-tracked: encoder doesn't
unroll strcpy/strlen byte-by-byte. Concrete cases use
recordKnownStringDestinationLength; symbolic cases bail. The real
Z3 opportunity is a capability addition, not a perf win.

Z4's matrix insertion site (heap_model.cpp::mergeFrom) was wrong --
that path is pure C++ hashmap union with no z3 queries. Real site
is encoder.cpp:688/715 where memcpy/strcpy unroll per-byte
store-chains capped at kMaxModeledCopyBytes and bail on symbolic
copy_bytes. Array lambdas would replace the loop body with one
expression and handle symbolic length.
2026-05-25 10:13:55 -04:00
trvon
72bc61f2c4
Document library expansion audit results 2026-05-25 09:55:55 -04:00
trvon
43827fa780
docs(matrix): mark Z2 unscheduled -- design FP
HeapRegion is a C++ struct, not a serialised z3 value. Its only
z3-typed fields (base, size) are already z3 BV exprs; freed/in_loop
are plain bools. The struct never crosses TUs, so a z3 tuple sort
saves zero bytes in the cross-TU cache. mergeFrom is hashmap union
with no z3 queries -- "alias-query count" was an imaginary timer.

Real opportunity at this site is path-sensitive freed merge, but
that's a heap-model correctness fix, not a library-expansion row.
2026-05-25 09:55:37 -04:00
trvon
c29a290be2
Reduce Curl high-signal false positives 2026-05-25 09:49:07 -04:00
trvon
6b5cae57a3
docs(matrix): mark Z1 unscheduled -- measured FP on curl
Instrumented `instantiateSummaryExpr` + `instantiateSummaryPredicate`
+ `instantiateSummaryPredicateInContext` directly: 3,052 calls /
238 ms on a full curl scan = 0.042% of analyze_ms (570 s).

Original `summary_lookup_ms` hypothesis was wrong-tracked; that
timer measures the hash-map lookup, not the parse_string path Z1
would replace. Quantifier e-matching would also add semantic-drift
risk for no measurable win.
2026-05-25 09:47:48 -04:00
trvon
07a39e73a7
perf(symbolic): compact serialised summaries via z3 let-bindings
Drop pp.min-alias-size from 10 (default) to 2 so shared subterms
collapse under `let` in serialize(). Measured 39.9% byte reduction
on encoder-shaped n=100 corpus (94.9KB -> 57.0KB). Roundtrip
preserved -- deserialize() parses let-form via z3's SMT-LIB reader.
2026-05-25 09:19:13 -04:00
trvon
1a7b6c1302
feat(detect): deprecated-api detector via clang::Decl::isDeprecated
New AST-only detector that flags call sites whose target decl is
marked deprecated under any of the three idioms:

- `[[deprecated]]` / `[[deprecated("why")]]` (C++14 attribute)
- `__attribute__((deprecated))` (GCC/Clang extension)
- `__attribute__((availability(...,deprecated=N.M)))` (Apple)

Per-call rather than per-decl so a migration that updates call
sites one at a time shows monotone progress. Deprecation reason
(when present) carries through into finding.message. System-header
call sites are skipped so the report stays scoped to the user's
own code.

Curl baseline: 52 real findings, e.g.
  ldap.c:269  ldap_url_parse — first deprecated in macOS 10.11
  multi-formadd.c:50  curl_formadd — since 7.56.0. Use curl_mime_init()

Refs docs/library-expansion-matrix.md row C7.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 00:47:38 -04:00
trvon
634607e1ea
perf(security): add detector sink slicing hooks 2026-05-24 23:45:12 -04:00
trvon
3c02c123f6
feat(symbolic): /*@ requires p != null */ contract seeding
Adds fold::symbolic::parseFunctionContracts(fd, ctx) — reads
RawCommentList for a function decl and returns the param indices
proven non-null by `/*@ requires <param> != null */` clauses.
SecurityWalker calls applyInitialFunctionContracts before walkCFG,
so within-function derefs see the contract via the existing
isDefinitelyNonNull pre-filter.

Grammar v1: bare `<ident> != null|NULL|nullptr`. Multi-line and
multi-clause supported. Unrecognised shapes are silently rejected
so future grammar growth doesn't break existing files.

applyHostToolchain now passes -fparse-all-comments. Without it
clang's lexer drops ordinary block comments before they reach the
AST, so the contract syntax can't ride along on `/*` ... `*/`.

Scope: suppresses within-function derefs on contracted params.
Does NOT suppress call-site findings — that needs CrossTUCache
integration so callee CalleeProperties carry the precondition.
Filed as a follow-up on matrix row C8.

Refs docs/library-expansion-matrix.md row C8.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-24 23:31:32 -04:00
trvon
64247e9409
docs: library-expansion matrix for Z3 + Clang opportunities
Catalogs 10 Z3 + 10 Clang APIs fold doesn't yet use, each with a
file-cited insertion site, scored payoff, and complexity/risk
columns. Top-3 sequencing nominates Z8 (unsat-core caching) →
C5 (UninitializedValues measurement) → C8 (RawCommentList).

Z8 + C5 ship in this same round; matrix rows updated with
"shipped" status. Perf-audit doc gains a single line pointing at
the matrix from its Phase-2 backlog header — the matrix is the
canonical surface for library-expansion picks now.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-24 23:03:02 -04:00
trvon
84a3126c35
feat(symbolic): wrap clang::UninitVariablesAnalysis
Adds fold::symbolic::runUninitAnalysisOnTU(ctx) returning aggregate
counts of functions with local read-before-write uses, derived from
clang's UninitVariablesAnalysis. Scope reminder pinned by the 4
unit tests: this flags LOCAL variable uninit reads, NOT parameter
nullability. The original matrix C5 framing ("null-deref FP triage
pre-pass") was wrong-hypothesis — the analyzer treats parameters as
always-initialised, so it cannot drive null-deref triage.

CFG build options need AddInitializers + setAllAlwaysAdd before the
per-fn AnalysisDeclContext sees decl initializers; without that the
analyzer silently reports zero uses.

Scan-pipeline integration deferred — needs a per-TU accumulator
threaded through the priming consumer.

Refs docs/library-expansion-matrix.md row C5.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-24 23:02:39 -04:00
trvon
42c5936e22
perf(security): cache unsat cores in sign_conversion + int_truncation
Mirrors the unsat_cores_ + unsat_core_path_hash_ + buildAssumptions +
rememberUnsatCore pattern that already shipped in null_deref,
div_by_zero, oob_access, and int_overflow. Prunes redundant path
predicates when consecutive queries on the same path return unsat.
Zero semantic risk; cores only narrow assumption vectors.

taint_track skipped: it does not query the solver.

Refs docs/library-expansion-matrix.md row Z8.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-24 23:01:29 -04:00
trvon
58f392899c
Add precision-preserving profiling prechecks 2026-05-24 20:31:55 -04:00
trvon
783d83cc45
Deduplicate null callsites and honor loop aliases 2026-05-24 20:02:53 -04:00
trvon
6c38999e60
Honor validated divisors and bounded snprintf 2026-05-24 19:55:43 -04:00
trvon
2c8263fc1c
Recognize bounded divisor and index patterns 2026-05-24 19:41:27 -04:00
trvon
b3d7791aed
Skip pointer arithmetic overflow summaries 2026-05-24 19:17:07 -04:00
trvon
473dcb8a79
Avoid UI helper arithmetic summaries 2026-05-24 18:48:37 -04:00
trvon
e3cd897aa9
Suppress bounded local counter overflow FPs 2026-05-24 18:30:38 -04:00
trvon
d9477545a0
Use promoted arithmetic widths 2026-05-24 18:18:11 -04:00
trvon
57f13a7643
docs(build): canonical BUILD.md with per-platform deps
Top-level BUILD.md so users see the dependency list (libz3-dev,
libclang-dev, llvm-dev, etc.) without spelunking docs/. Lists apt
package names for Ubuntu/Debian, dnf names for Fedora/RHEL, and
brew names for macOS — kept in sync with docker/Dockerfile.linux-
validate, which is the executable spec.

- README points at BUILD.md instead of docs/BUILDING.md.
- docs/BUILDING.md trimmed to radare2 vendoring notes only (the
  topic that didn't belong in a top-level quick-start).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-24 18:11:31 -04:00
trvon
600f8020a6
build(setup): portable LLVM detection on Linux
setup.sh only checked /opt/homebrew/Cellar/llvm/* and exited with
"No Homebrew LLVM installation found" on any non-macOS host. Add
Linux probing for /usr/lib/llvm-N/bin and llvm-config-N on PATH,
plus a brew --prefix llvm@N fallback for Intel/custom-prefix macOS.

Also:
- Symlink z3.pc from the arch-dependent /usr/lib path into the
  meson build dir on Linux (libz3-dev installs there but meson
  doesn't search it by default).
- Thread the discovered llvm-config path through to meson via
  -Dllvm-config-path so the fold meson option picks up the right
  binary instead of guessing.
- Default CC/CXX to the discovered clang when unset; necessary on
  apt-installed Linux where bare `clang`/`clang++` may not exist.
- Honor caller-provided LLVM_BIN env override.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-24 17:41:04 -04:00
trvon
0a92540b63
Prove positive derived divisors 2026-05-24 13:03:45 -04:00
trvon
9fd5b2b1ec
ci(docker): linux validation harness mirroring GitHub Actions
Ubuntu 24.04 + clang-19 + Conan-built deps, replicating the
build.yml Linux job step-for-step. Runs locally via:

    docker build -t fold-linux-validate -f docker/Dockerfile.linux-validate .
    docker run --rm -v "\$PWD":/workspace:ro fold-linux-validate

Catches cross-platform issues (libstdc++ vs libc++ AST shape
differences, transitive-include divergence, pimpl + forward-decl
unique_ptr instantiation) before the GitHub Actions round-trip.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 21:54:47 -04:00
trvon
68326d617f
fix(dead-statement): skip system-header function bodies
libstdc++'s std::unordered_map / std::vector template instantiations
shape their bodies in ways that trip the unreachable-block detection
(structured-binding desugaring inside range-for body blocks).
libc++ on macOS happens to surface a different AST shape; the filter
keeps the signal cross-platform. Surfaced by the Linux validation
container against the M28 dead-statement regression test.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 21:54:27 -04:00
trvon
976b667e45
Improve Clang guard handling 2026-05-23 21:47:53 -04:00
trvon
9707b35872
Propagate positive divisor initializers 2026-05-23 21:23:17 -04:00
trvon
03d93c0830
Refine divisor guards with Clang AST patterns 2026-05-23 21:06:37 -04:00
trvon
106fc07fe7
Use AST guards for divisor precision 2026-05-23 20:50:13 -04:00
trvon
331a68467c
Optimize security detectors and add quality gates 2026-05-23 20:42:05 -04:00
trvon
386f700575
refactor(symbolic): extract z3_helpers (supportsParam, collectConjuncts)
Same fold-on-fold pass that flagged mixHash duplication also caught
supportsParam (7 copies) and collectConjuncts (4 copies) across the
security detectors + class_dup + equiv + dead_branch.

Move both to a new src/symbolic/z3_helpers.{hpp,cpp} sibling of
the existing z3_model_support / expr_hash / tactics. Detectors
add `using fold::symbolic::supportsParam;` (and collectConjuncts
where used) in their anonymous namespaces so call sites stay
unchanged.

Net: -110 lines across 7 detectors. No behavior change.

44/44 tests still green.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 15:48:03 -04:00
trvon
db1a1d556e
refactor(detect): drop duplicate mixHash, use fold::mix64
5 copies of the same FNV-1a-style mix function across
div_by_zero / int_overflow / int_truncation / sign_conversion
(security detectors) + incremental_cache. Each was byte-identical
to fold::mix64 in src/ir/normalize.cpp.

Replace with the existing fold::mix64. Net: -30 lines, no
behavior change.

Surfaced by the fold-on-fold pass (commit 57cf9fd) which flagged
this exact duplication as a clone cluster.

44/44 tests still green.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 15:41:43 -04:00
trvon
159e71ec1a
bench: 5-run post-cache baseline for the curl workload
Captured via bench/perf_audit_run.py --label baseline-post-cache
--runs 5. State of the tree: writtenvars cache (52218478) +
dead-statement range-for filter (8895d50) + all phase-timing
instrumentation from this audit session.

5-run summary:
  observed_wall_s            436.6s  iqr=17.8s  range=427.6..452.1
  total_wall_ms              436,568 ± 17,837
  scan.serial_dispatch_ms    436,498 ± 17,836  (all of it; --profile forces serial)

  detector breakdown (medians, ms):
    null-deref   wall=270,437  walker=267,126  encode=6,109  hook=4,748  z3_queries=5,531
    dead         wall=8,526
    equiv        wall=2,173
    clone        wall=204

The 4% IQR is tight enough to attribute sub-5% optimizations
reliably; bigger changes need fewer samples. Future perf PRs
should `--diff docs/perf-audit/runs/baseline-post-cache.json`
against this snapshot.

Also archives the earlier 2-run smoke output as
docs/perf-audit/runs/smoke.json.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 15:17:16 -04:00
trvon
9d08cb73a9
docs(perf-audit): update fold-on-fold triage with dead-statement fix outcome
Document the 152 → 16 reduction from the range-for + structured-
binding filter (commit 8895d50). The remaining 16 findings are
all the same idiom: defensive `return "unknown"` after an
exhaustive enum switch. Lays out three resolution options and
recommends documenting it as a known FP idiom rather than
refactoring fold's own source.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 14:51:01 -04:00
trvon
8895d50015
fix(dead-statement): filter range-for + structured-binding FPs
Fold-on-fold pass surfaced 152 dead-statement findings against
fold's own source. Spot-checks revealed every flagged statement
was inside a range-for body. Minimal isolation fixture:

  for (const auto& [k, v] : src)
      dst[k] = v;            // flagged FP: "unreachable from entry"

Plain `for (int x : v)` does NOT trip the bug. The trigger is the
structured binding inside the range-for. Clang's CFG for that
shape leaves the body block without an incoming edge from the
loop's condition check, so the reachability walk in
DeadStatementVisitor marks every body statement unreachable.

Filter: walk up the parent map from the candidate anchor (capped
at 16 hops); if any ancestor is a CXXForRangeStmt, suppress the
finding. Narrow, behaviour-preserving for real dead code,
contained to the same defensive-filter block that already handles
macro-expanded anchors.

Result on fold's own 134-TU compile DB:
  before: 152 dead-statement findings
  after:   16  (90% reduction)

The remaining 16 likely have other root causes; future audit work.

Added regression test exercising the exact failing fixture; 5/5
test_dead_statement cases pass. Full 44/44 suite still green.

Note: this is itself a fold-on-fold success — the perf-audit's
fold-on-fold pass surfaced a detector bug that had been silently
shipping FPs on every C++ codebase with structured-binding range
loops.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 14:48:38 -04:00
trvon
28c5155bfa
docs(perf-audit): point at bench/perf_audit_run.py for future PRs
The audit's "measurement discipline" section now names the
concrete tool to use for any sub-15% perf claim — the multi-run
harness committed in the previous commit. Includes the
copy-paste invocation pattern for before/after diff.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 13:25:59 -04:00
trvon
21886984af
bench: multi-run perf measurement harness with median+IQR
Adds bench/perf_audit_run.py which runs fold N times against a
fixed compile DB and reports median + IQR + min/max for every
metric in --profile JSON (scan-level + per-detector). Output
snapshots land under docs/perf-audit/runs/<label>.json so
before/after diffs are git-tracked.

Motivation: the audit's writtenvars-cache commit (5221847)
showed sub-15% perf moves can't be reliably attributed from a
single run — cross-baseline variance was ~15% just from system
load. Future perf PRs need a statistical baseline. This harness
is the single-machine version of that discipline.

Usage:
  bench/perf_audit_run.py --label baseline --runs 5
  bench/perf_audit_run.py --label after-fix --runs 5 \\
      --diff docs/perf-audit/runs/baseline.json

The --diff mode prints a side-by-side with significance flagged
("yes"/"no") when the delta exceeds 1.5×IQR. That's a rough rule
of thumb; for high-confidence claims, run 10+ samples.

Smoke-tested with --runs 2 (15min wall) on the curl baseline:
2-run IQR = 13s on a ~440s scan (~3% band). 5 runs should
tighten that.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 13:25:21 -04:00
trvon
52218478f9
perf(walker): cache collectWrittenVars per Stmt (audit remediation #1)
WriteCollector is a RecursiveASTVisitor over a stmt subtree.
5 call sites in security_walker.cpp consulted it per-statement
during the main walk, the buildNonNullAtBlockEntry pre-pass, and
unrollLoop. Same stmt subtrees re-walked across these sites.

Add cachedCollectWrittenVars(stmt) member function with a
per-SecurityWalker std::unordered_map<const clang::Stmt*, set>
cache. WriteCollector's output is a pure function of the stmt
subtree (no SymbolicEnv state, no ctx mutation), so the cache
is always safe. Map lives one function's analysis (SecurityWalker
is per-function-scoped).

Converted all 5 call sites to the cached wrapper.

Perf: ran the curl baseline after the change (4 workers, 306 TUs,
2339 functions). Single-run wall = 451s, vs. 442–465s spread across
prior single-run baselines. Within run-to-run variance; the audit
doc adds a "measurement discipline" section noting that 5+ runs +
median are needed to validate sub-15% wins. Cache is architecturally
sound regardless — pure memoization with no behavior change.

44/44 tests still green.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 13:02:06 -04:00
trvon
8c154612b9
perf(profile): hook_ms — fourth pivot, walker bookkeeping IS the cost
Add a ScopedHookTimer RAII helper to SolverStats and instrument the
6 hot per-detector hook bodies (onPointerDeref, onDivision,
onBinaryArith, onTruncation, onSignedUnsignedConversion,
onArraySubscript). Each now records its own hook_ms which surfaces
per-detector in --profile JSON.

For null-deref on the curl baseline:

  wall_ms:                 344,644
    walker_ms:             340,329
      encode_ms:            24,156   (7.1% of walker)
      hook_ms:              11,860   (3.5% of walker)
      state_merge_ms:           75
      [unaccounted]:       304,238   (89.4% — walker bookkeeping)

Z3 total: ~33ms across the entire scan.

This is the fourth wrong-hypothesis pivot in this audit:
1. Hash-walk fusion: 0.04% of total          ← wrong
2. State-merge optimization: <0.1%           ← wrong
3. Encoder optimization: 7.1%                ← wrong
4. Hook body optimization: 3.5%              ← wrong

The 304s lives in the walker's per-stmt bookkeeping:
- M22-M28 non-null marking (6 separate accumulators)
- collectWrittenVars (per-stmt RecursiveASTVisitor)
- refineRanges / refineOctagon (per-branch)
- buildNonNullAtBlockEntry (per-function pre-pass)
- BranchLivenessBuilder / WriteCollector ancillary passes

Phase-level instrumentation can't subdivide further without
O(stmts) timer overhead. Updated docs/perf-audit.md with
flame-graph-based profiling as the next step + 4 specific
remediation candidates ranked by likely payoff.

44/44 tests still green.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 12:41:54 -04:00