Commit graph

122 commits

Author SHA1 Message Date
trvon
5b5fd7dbc6
feat: binary stack-buffer-overflow detection via SSA P-Code
Phase 5: detect stack buffer overflows in binaries through the P-Code
pipeline, value-aware against recovered buffer capacities.

- ir::pcode::PcodeOp gains in_buffer_sizes (parallel to in): per-pointer-arg
  recovered stack-buffer capacity, SSA-only (-1 = unknown).
- ghidra_lifter: inject locked prototypes for known copy/input sinks at their
  call-target addresses BEFORE the SSA action pipeline, so parameter recovery
  attaches the destination pointer; resolveStackBufferBytes traces the
  PTRSUB/PTRADD/INT_ADD spacebase+const def chain to a stack address and reads
  the recovered local's type size via getScopeLocal()->findOverlap().
- pcode_encoder: recognize copy/input sinks (unbounded: gets/strcpy/strcat/
  sprintf/...; bounded: fgets/read/memcpy/strncpy/snprintf/...) and fire
  onBufferCopy with the write length when the dst capacity was recovered.
- BinaryBBObserver::onBufferCopy: ugt(size, cap) bounds check -> OOBAccess
  finding (binary-buffer-overflow). Unbounded sinks -> Heuristic; bounded with
  provable len>cap -> Proved (checkAndEmit gains a confidence parameter).
- router: SSA rewrites CALL targets to synthesized addresses, so resolve callee
  names from the raw stream by target address and carry them to the SSA stream
  by call-site instruction address (annotateByTarget / annotateBySite).
- r2_backend: extractCallTargetNames maps stub/func addresses -> normalized
  callee names; scan_runner activates the lane for oob/oob-access detectors.

Validated on bench/corpora/ctf-challenges: 3 TPs in libbof_overflow
(gets->32B, strcpy->64B, sprintf->128B), genuine TPs in libcmd_injection
(sprintf->256B) and libvuln_demo (strcpy->16B), zero false positives across
the corpus; safe_version and the bounded __strncpy_chk path are not flagged.
Unit tests cover unbounded/bounded/safe/no-size/non-sink cases.
meson test: 74/74. div-by-zero and null-deref P-Code findings unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 00:09:07 -04:00
trvon
c10b99961f
fix(binary): bound r2 security scans
- add r2 CLI timeouts and cap structural sweeps
- fix SQL return-source taint and base64 blob filtering
- document vendor lanes and Ghidra .sla runtime policy
2026-05-31 22:49:56 -04:00
trvon
79596f2f81
feat(binary): null-deref via stack memory model in P-Code encoder
Adds a second hazard to the P-Code encoder. LOAD/STORE now fire onPointerDerefIr
when the dereferenced address provably folds to 0, backed by a small stack-slot
memory model (address-value -> stored value) so a `store 0,[slot]; load r,[slot];
deref r` chain proves r==0. Gated on a provably-zero address (no path forking
yet) → zero-FP: ordinary stack accesses (sp+const) and heap pointers don't fold
to 0. The value map keys on (space,offset) so sub-register reads alias the
full-register write the lifter emits.

Validated end-to-end (ghidra-enabled) on the CTF corpus: libnull_deref.dylib ->
2 null-deref TPs (sym._null_deref_direct AND sym._null_deref_conditional — the
const-0 pointer flows linearly so the conditional case is caught without path
forking), matching the source detector's 2 proved null-derefs; div-by-zero still
2 on libdiv_by_zero; zero null/div FP across the other corpus dylibs. Unit tests
(test_pcode_encoder): store-0->load->deref fires; ordinary stack-slot deref does
not. Full suite 71 Ok / 0 Fail / 1 pre-existing binary_backend timeout.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 20:57:10 -04:00
trvon
b560e696ba
fix(test): drop binary_bb_encoder references from test_binary_bb_observer
Follow-up to 3fa6d0f56 (dead-encoder removal): test_binary_bb_observer.cpp had
a stale `#include "fold/symbolic/binary_bb_encoder.hpp"` and three TEST_CASEs
exercising the deleted encodeBinaryBBs. The earlier commit's targeted build did
not compile this test, so the break only surfaced in the full suite. Remove the
stale include and the obsolete encodeBinaryBBs cases; the direct
BinaryBBObserver hook tests (null-deref / div-by-zero / overflow) remain and
pass. Full suite 71 Ok / 0 Fail / 1 timeout.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 20:56:55 -04:00
trvon
1681acbee6
feat(binary): dual-stream P-Code router + first end-to-end finding (Phase 3.5)
Wires the whole pipeline together: binary bytes -> raw+SSA lift -> P-Code
encoder -> Z3 -> BinaryBBObserver -> merged finding.

- include/fold/binary/pcode_router.{hpp} + src/binary/pcode_router.cpp:
  routeFunctionToFindings() lifts a function both ways (liftRaw + liftSSA),
  encodes each into a fresh Z3 context/observer, tags findings with the lift
  level (evidence.backend = binary-pcode-raw|ssa), and merges + dedups by
  (kind, line, detector) — raw wins on collision (more direct hazard evidence).
- meson: gated `fold-ghidra` static lib (ghidra_lifter + pcode_router) linking
  fold_core_dep + ghidra_decompiler_dep (first fold_core <-> Ghidra link), and
  the `pcode_scan` smoke driver. Defined after fold_core so both deps are in
  scope; built only when -Dghidra=enabled.
- pcode_encoder fix: value map now keys on (space, offset) without size and
  coerces on read, so a sub-register divisor read (w9, size 4) sees the
  full-register zero write (x9, size 8) the lifter emits for `mov w9,#0`
  (ARM64 zero-extend). New unit test locks this aliasing.

Validated end-to-end: pcode_scan on arm64 sym._div_by_zero_direct emits exactly
one finding — `[binary-pcode-raw] division by zero proven by Z3` — with the SSA
stream correctly contributing nothing (it folds x/0 -> 0) and the router
merging. Full suite 71 Ok / 0 Fail / 1 pre-existing binary_backend timeout.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 20:02:03 -04:00
trvon
15d32eaec8
feat(binary): P-Code -> Z3 encoder MVP — div-by-zero (Phase 3)
src/symbolic/pcode_encoder.{hpp,cpp}: a register-machine symbolic executor over
fold::ir::pcode (provenance-agnostic — consumes Raw or SSA). Assigns a Z3 BV per
varnode, propagates values through COPY / INT_ADD/SUB/MULT / AND/OR/XOR /
ZEXT/SEXT/SUBPIECE, and fires the existing SafetyObserver::onBinaryArith(Div)
hook for INT_SDIV/UDIV/SREM/UREM so BinaryBBObserver proves the hazard. Lives in
fold_core (no Ghidra dep); it is the real encoder the dead abstract-block path
never was.

MVP precision: the Div hook fires only when the divisor constant-folds to 0
(zero-FP without path forking — a bare symbolic divisor would make SAT(rhs==0)
trivially true and FP on every `a/b`). NEXT: CBRANCH path-forking drops this
gate (catches data-dependent zero divisors); null-deref (LOAD/STORE + stack
memory model, const-0 address gating) and guarded int-overflow follow.

tests/unit/test_pcode_encoder.cpp (no Ghidra/r2): constant-zero divisor and
COPY-chained zero are flagged; non-zero and bare-symbolic divisors are not.
Full suite 71 Ok / 0 Fail / 1 pre-existing binary_backend timeout.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 19:51:05 -04:00
trvon
f4f59ce23c
feat(binary): constant-zero div-by-zero + null-deref over disassembly
Abstract-block mode fired one over-approximate onBinaryArith per block with
fresh unconstrained operands, so it proved nothing for div-by-zero / null-deref
(0 binary findings on libdiv_by_zero / libnull_deref). A full per-instruction
symbolic encoder with path modeling is the eventual fix, but on symbolic args
an unconstrained overflow/aliasing check is trivially SAT and would fire on
every add/sub/mul (the FP blowup) — so this slice lands the sound, zero-FP
subset: constant-zero propagation.

scanInstructionsForConstHazards (pure, unit-tested): a per-function state
machine tracking which registers / stack slots provably hold 0.
  mov r,0 | mov r,wzr | str wzr,[slot] | ldr r,[zeroed-slot]  -> r/slot is 0
  sdiv/udiv d,a,b with b==0                                   -> div by zero
  ldr/str through a zero base register [x0]                   -> null deref
Tracking resets at every branch / call / ret, so it never reasons across an
unmodeled path — no false positives, at the cost of missing path-dependent
zeros (div_by_zero_variable, null_deref_conditional remain FN: those need
branch modeling, deferred).

Both structural disasm passes (this + UAF) now share one per-function pdfj
sweep via disasmFindingsInSession; const-hazard scanning runs default-on
alongside the import/string detectors and under --detectors
security/div-by-zero/null-deref. Findings carry Heuristic confidence.

Validated on the CTF corpus: +3 TP — div_by_zero_direct, mod_by_zero
(CWE-369), null_deref_direct (CWE-476), all previously FN in binary mode —
with zero new findings across the other 8 dylibs. 6 r2-free unit tests
(test_binary_const_hazards) cover literal-zero divisor, slot-propagated null
deref, non-zero/unknown divisors, branch-reset, and non-zero pointer
negatives. 70 Ok / 0 Fail / 1 pre-existing binary_backend timeout.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 15:02:43 -04:00
trvon
5294e6bdf9
feat(binary): structural use-after-free / double-free over disassembly
Binary mode had no UAF/double-free detection. In abstract-block mode the Z3
encoder gives every call argument a fresh, unconstrained constant, so an
aliasing check (freed_ptr == deref_ptr) is trivially SAT for unrelated
pointers — it would fire on every post-free deref. A Z3-proven aliasing UAF
is therefore not viable until per-instruction value flow lands.

Instead detect it structurally over r2 per-instruction disassembly, keyed on
the *stack slot* a heap pointer lives in (r2 names it, e.g. `[ptr]`/`[dest]`)
— a reliable identity within one function, no aliasing needed:

  scanInstructionsForUaf (pure, unit-tested): a per-function state machine.
    ldr <reg>,[slot]  -> reg sources slot
    bl free           -> slot dangling (double free if already dangling)
    bl <other>        -> use-after-free if the freed slot's pointer is the arg
    str <alloc>,[slot]-> slot revived (free+realloc+free is legitimate)
  detectUseAfterFreeFromDisasm / uafFindingsInSession: r2 wrapper, run from
    R2Backend::runDetectors on the shared session (no extra `aaa`), default-on
    alongside the import/string detectors and under --detectors
    security/use-after-free/double-free. Findings carry Heuristic confidence.

Cost is bounded: skip entirely when the binary imports no free/delete (instant
on the common case) and cap the per-function pdfj sweep at 4000 functions.

Validated on bench/corpora/ctf-challenges/libuaf_doublefree.dylib: +3 TP
(double_free_bug CWE-415, use_after_free_bug + use_global_after_free CWE-416 —
all previously FN in binary mode), zero new findings across the other 9 corpus
dylibs. 5 r2-free unit tests (test_binary_uaf) cover double-free, UAF,
clean-free, free+realloc+free, and cross-slot negatives. 69 Ok / 0 Fail /
1 pre-existing binary_backend timeout.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 14:49:10 -04:00
trvon
2c4f5880e5
fix(null-deref): path-sensitive &&/|| guard, not source-text (A1)
`if ((p != 0) && (p->m))` and `if (p == 0 || p->m)` false-positived: the
walker's encodeConditionWithShortCircuit added the LHS to the Z3 solver for
the RHS encode but never updated the env's non-null marks — and the deref
gate checks `env.isDefinitelyNonNull`, not the solver. So only a fragile
same-line text heuristic (sourceLineShortCircuitProvesNonNull) suppressed the
canonical `p && p->m`; parenthesized / `!=0` forms slipped through.

Fix: in the &&/|| short-circuit handling, extract the LHS null-check
(extractNullCheckedVar) and mark its pointer definitely-non-null for the RHS
encode only, then clear it — short-circuit guarantees the LHS holds when the
RHS runs, and clearing prevents the fact leaking into the else-arm (where the
pointer may be null). Path-sensitive, shape-independent, and the bitwise `&`
case stays flagged (both operands really evaluate).

Drift gate: no per-detector change (null-deref 0.792/0.494/0.609 held) — the
fix removes a real FP class without moving the capped-corpus numbers and is
more robust than the text heuristic it supersedes.

Tests: 3 cases in test_null_deref (parenthesized && safe, || safe, bitwise &
still flagged).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 12:50:31 -04:00
trvon
080bf9655f
feat(walker): affine (symbolic-bound) loop summarization + deterministic baseline
Extend ir::Loop classification: a monotone single-induction-variable for-loop
with a symbolic bound (e.g. `for (i=0; i<n; ++i)`) is now Affine (was General).
The walker summarizes Affine loops like Counted ones — bind the induction
variable to a fresh symbolic, assert its start bound plus the loop condition
(encoded to a Z3 bool) as path constraints, walk the body once — exposing
high-index OOB that bounded unroll misses. Findings are over-approximations
(LoopHavoc/heuristic), never proved.

Net effect on the gate: oob recall 0.531 -> 0.559, F1 0.637 -> 0.652 (precision
dips 0.796 -> 0.782) — a contract-sanctioned over-approximation tradeoff.

Also regenerate bench/fp-baseline.json deterministically from a clean run: the
prior committed oob numbers (0.574) were not reproducible (transient warm-state
artifact); fresh runs are now bit-identical, so the gate is reliable.

Tests: test_ir_loop (affine classification), test_loop_summary (symbolic-bound
OOB exposed, not proved). Suite green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 08:46:16 -04:00
trvon
d73ba7ea8f
feat(walker): counted-loop summarization with over-approx tagging (B2)
For a counted loop whose trip count exceeds the unroll bound, bind the
induction variable to its full closed-form range (asserted as a Z3 path
constraint, not just interval metadata) and walk the body once, instead of
unrolling loop_unroll times. This fires each in-body sink against all
iterations at once: it exposes high-index OOB that bounded unroll misses
(oob recall 0.531 -> 0.574, F1 0.637 -> 0.658 on the gate) and collapses
per-iteration queries to one. Findings from the summarized walk are
over-approximations and are tagged LoopHavoc (heuristic) confidence, never
proved. General/while(1) loops keep the bounded-unroll fallback.

Per the precision contract (loop-summarization-research.md), this is a
recall-improving over-approximation that may add bounded, tagged FPs. The
drift gate is updated to match: it now fails on recall- or F1-drop beyond
tolerance (the regressions it was built for) and allows precision dips that
come with recall gains; the committed baseline is regenerated to the B2
numbers.

Tests: test_loop_summary (high-index OOB found + not proved; in-bounds loop
clean). Suite 68/0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 22:44:01 -04:00
trvon
797759d0b6
feat(ir): frontend-agnostic ir::Loop + clang lowering (B1 slice 1)
Foundation for core loop summarization (Workstream B). Adds a clang-free
ir::Loop / InductionVar / LoopKind (Counted | Affine | General) mirroring the
M30 ir::Expr pattern (Symbol-keyed, opaque frontend handle, lowering in
src/symbolic/ir_loop_clang.cpp).

buildLoopFromClang classifies counted `for` loops (constant init/stride/bound,
ascending or descending) into a Counted loop with a known trip count; affine
and unrecognized shapes (while(1) scanners, data-dependent bounds) stay
General and will use the bounded-unroll fallback. The symbolic core will
consume ir::Loop instead of clang::ForStmt so summarization is written once
and reused by the rust/binary frontends.

Tests: test_ir_loop (counted asc/strided/desc trip counts; while(1) and
data-dependent bound -> General). Suite 67/0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 18:11:29 -04:00
trvon
4bc62d721a
fix: scan namespaced/nested functions; timeout robustness; null-deref drift
Enumeration: replace the hand-rolled top-level `tu->decls()` walks (TU
security prefilter + every interprocedural pre-pass/cross-TU primer in
div-by-zero/int-overflow/null-deref/oob) with a shared RAV-based
`fold::detect::forEachFunctionWithBody`, so functions nested in namespaces
and classes are analyzed. Fixes 0-findings on namespaced C++ (dbench CTF).

TU prefilter: extracted to fold/cli/tu_security_prefilter; flags unsigned
-/* wraps so int-overflow candidates in size arithmetic are not skipped.

Timeout robustness: cooperative per-TU walk deadline (WalkOptions) checked
in the walk/unrollLoop/CFG-block checkpoints and the BatchSafetyObserver
flush loop, plus a non-progress unroll fixpoint — slow TUs degrade with
partial findings instead of being SIGALRM-quarantined.

OOB precheck: treat `*(p+i)` and indirect callees as candidates.

null-deref drift fix: remove the unsound `priorFunctionTextProvesNonNull`
source-text heuristic (added in 331a68467) that treated a textual `p!=0`
before a deref as proof of non-null, silently suppressing real null-derefs
(CWE-476 F1 0.485 -> 0.583, recall 0.379 -> 0.517).

Tests: test_function_enumeration, test_tu_security_prefilter, namespaced
cross-TU int-overflow regression. Full suite green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 18:04:15 -04:00
trvon
dcbbba73ff
fix: interprocedural buffer-bound pre-pass verification + test
- Verified seedInterproceduralContext() pipeline: SecurityWalker calls it
  before TraverseDecl, OOBObserver scans all call sites for ConstantArrayType
- OOB observer consumes param_buffer_sizes_ for ParmVarDecl base variables
- Test added for interprocedural buffer-bound detection
2026-05-30 00:48:07 -04:00
trvon
c3db07eda3
fix: pass ParmVarDecl to OOB observer for interprocedural buffer tracking
- encoder.cpp: extract VarDecl from ArraySubscriptExpr base when
  trackedArrayVar returns null (covers ParmVarDecls like 'output')
- test_oob_access.cpp: test interprocedural buffer propagation
  (decode_base64 receiving decoded[256] via pointer parameter)
- Root cause: validate_auth is summarized, preventing inline walk.
  Mitigation tracked in docs/internal/interprocedural-notes.md
2026-05-29 23:40:25 -04:00
trvon
9be8deb4f6
feat: enrich findings with path trace, counterexample, variable info
- SizeInfo struct: buffer_name, capacity, index_expr for OOB findings
- path_trace: vector of branch/call events from SafetyObserver
- counterexample: Z3 model key=value pairs for SAT findings
- variable_name, sink_function: named context for LLM steering
- Serialized in FindingEvidence + SARIF properties
2026-05-29 23:13:09 -04:00
trvon
8a465b6675
feat: BinaryBBObserver — Z3-proved binary findings + unit tests
- BinaryBBObserver extends SafetyObserver for binary BB analysis
- Detects null-deref, div-by-zero, int-overflow via Z3 on binary paths
- Wired into BinaryDetectorRunner in scan pipeline
2026-05-29 21:55:48 -04:00
trvon
b9af5afb63
feat: BinaryBBObserver — Z3-proved binary vulnerability findings
- SafetyObserver subclass for binary basic block analysis
- Detects null-deref, div-by-zero, int-overflow on binary paths
- Wired into scan pipeline via BinaryDetectorRunner
- Binary Z3 findings flow through standard SARIF/JSON output
- Expected: +6 Z3-proved findings on CTF corpus
2026-05-29 21:55:09 -04:00
trvon
e3f1302922
feat: BinaryIR type system — BinaryExprKind + BinaryIRTree
- 13 BinaryExprKind values: HardcodedString, ImportedSymbol, BasicBlock, etc.
- BinaryExprNode with kind-specific payload (symbol, section, xref, call)
- BinaryIRTree + BinaryIRBuilder for incremental tree construction
- Mirrors source ir::Expr architecture for binary analysis
2026-05-29 21:01:21 -04:00
trvon
cb600dbac9
feat: IR language discriminator + Clang node kinds (Phase 3, multi-language IR)
- Language enum: Unknown, Rust, Cpp, Swift on RustExprNode
- 4 new Clang node kinds: ThrowExpr, CXXConstructExpr, LambdaExpr, StmtExpr
- Updated json_keys namespace with new constants
- 42 roundtrip test assertions pass
- All 3 frontends now share the same IR representation
2026-05-28 20:55:54 -04:00
trvon
98f02a4f4c
feat: architecture expansion — summary cache, observer interface, AI pre-pass
- SymbolicSummaryCache: per-function BV-64 cache to avoid re-encoding
- Language-agnostic observer overloads for Rust/Swift backends
- Abstract-interpretation pre-pass for Rust integer range filtering
- 3 new unit tests, 63/63 pass
2026-05-27 18:20:51 -04:00
trvon
9bdcd9f7a0
feat: audit fixes — complexity, CLI UX, MCP parity, binary fuzz
- CompositeSafetyObserver replaced with macros (composite_observer.hpp)
- tool_registry split per category
- Renamed --null-deref-nullable-allocations to --nullable-allocations
- --input-type hidden from help, fold-swift --help added
- MCP: fold.scan_rust, fold.scan_swift, backend filter on get_findings
- Binary fuzz: fold fuzz prep --binary <mach-o>, --compile-db optional
2026-05-27 16:29:39 -04:00
trvon
ffc7917957
feat: symbolic IR, swift backend, binary-to-fuzz pipeline, SARIF fixes
- Rust IR encoder (encodeRustIr): walks HIR expr trees, fires SafetyObserver hooks
- Swift SIL frontend (fold-swift): swiftc -emit-sil parser, demangling, NDJSON output
- SwiftBackend: --mode swift CLI routing, shells out to fold-swift
- Binary-to-fuzz: Mach-O export discovery, ARM64 ABI param-count inference, dlopen harness generation
- Language-agnostic IR: json_keys canonical constants, Call/Member/ArraySubscript/Cast/HeapNew nodes
- Dogfood fixes: SARIF kind field, cross-detector dedup, harness_file in predictions, Swift stoi safety
- Corpuses: swift-sil-z3-corpus.md, binary-fuzz-corpus.md
- AGENTS.md: cross-language wire format rule
2026-05-27 15:29:14 -04:00
trvon
ce1c353847
feat: harden security scanner routing 2026-05-27 10:36:57 -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
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
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
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
72bc61f2c4
Document library expansion audit results 2026-05-25 09:55:55 -04:00
trvon
c29a290be2
Reduce Curl high-signal false positives 2026-05-25 09:49:07 -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
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
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
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
0a92540b63
Prove positive derived divisors 2026-05-24 13:03:45 -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
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
da4c848cbc
feat(rust): MVP Rust frontend — unsafe-deref detection + clone clustering
First non-clang frontend for fold. Scans Rust source for the
subset of issues that meaningfully exist there: raw-pointer
dereferences in `unsafe` blocks (Heuristic confidence) and
within-Rust Type-1/2/3 clones via the existing clone detector.
Reuses the M30 frontend-agnostic IR + AnalysisBackend
abstraction unchanged.

Architecture: a separate `tools/fold-rust` cargo binary that the
C++ `RustBackend` shells out to. Two subprocess invocation
modes:

- `extract --unit <file>`: standalone .rs file, no Cargo
  context (limited; external-crate imports won't resolve).
- `cargo-check --crate-dir <dir>`: invokes `cargo check` with
  RUSTC_WORKSPACE_WRAPPER set to ourselves so cargo supplies
  the full --extern/-L/--cfg per-TU. Mirrors the
  clippy/miri/kani pattern.

RustBackend::discoverUnits walks up from each `.rs` input to
find the nearest Cargo.toml and collapses sibling files to one
crate unit; orphan files fall back to single-file extract.
scan_router auto-detects --mode=rust when all paths are `.rs`;
mixed Rust + C/C++ warns and defaults to source mode.

Includes:

- tools/fold-rust/: cargo crate pinned to nightly-2026-05-22.
  HIR-driven DerefHunter (raw-pointer dereference detection
  via typeck `is_raw_ptr()`) and HashWalker (FNV-1a + mix64
  port of src/ir/normalize.cpp for FunctionSummary hashes).
- include/fold/ir/expr_json.{hpp,cpp}: RustExprNode +
  RustNodeArena + bidirectional JSON wire format for
  ir::Expr; round-trip tested across 8 Kinds.
- include/fold/frontend/rust_backend.{hpp,cpp}: AnalysisBackend
  impl. posix_spawnp + NDJSON parser. Caches summaries +
  findings across extractFunctions calls; polymorphic
  runDetectorsOnShard dispatch routes summaries to
  detectClones.
- src/cli/{main,scan_router,scan_runner}.cpp: --mode=rust
  branch + autoDetectRust(). Mode validation extended to
  accept rust.
- docs/rust-frontend.md: scope, wire format, rustc-private
  pin + bump procedure, verification snippets.

44/44 unit + integration tests still green. Verified
end-to-end on a 3-function rename-pair fixture: 1 clone
cluster + 1 raw-deref finding through both `extract` and
`cargo-check` paths.

Out of scope (tracked separately): equiv + class-dup
detectors (need encoder frontend-agnostification),
dead-statement (needs Rust CFG), cross-language clones
(needs unified kind-tag taxonomy), Proved/ConcreteWitness
promotion (needs encoder integration), `cargo fuzz`
harnesses.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 22:36:28 -04:00
trvon
1fa0612f6e
feat(encoder): M28 phi-merge non-null facts in summary encoder
When both arms of an if-statement prove the same variable non-null
(via M22 null-check inference or M23 address-of marking), the
summary encoder's `mergeBindings` now intersects the two branches'
nonNullVars sets and marks the survivors in the post-if env.
Mirrors the same logic already present in
security_walker.cpp::walkIf so summary mode and the main walker
stay in parity.

Added tests/unit/test_summary_encoder_phi_nonnull.cpp covering
the rename-pair address-of case. Pre-fix the test FAILs; post-fix
PASSes. 44/44 still green.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 22:35:00 -04:00