Commit graph

372 commits

Author SHA1 Message Date
trvon
1ec7f71690
ci: use Forgejo artifact actions; disable unsupported cache
Some checks failed
build / build-linux (push) Failing after 7m32s
build / build-macos (push) Has been cancelled
build / release (push) Has been cancelled
2026-08-30 10:34:24 -06:00
trvon
6140ba44de
ci: install CMake build tools for Conan dependencies
Some checks failed
build / build-linux (push) Has been cancelled
build / build-macos (push) Has been cancelled
build / release (push) Has been cancelled
2026-08-30 10:31:45 -06:00
trvon
43e5de4f0a
ci: bootstrap rustup on Forgejo runner
Some checks failed
build / build-linux (push) Failing after 1m12s
build / build-macos (push) Has been cancelled
build / release (push) Has been cancelled
2026-08-30 10:28:39 -06:00
trvon
df0d984b04
ci: split linux/macos jobs; gate macOS behind workflow_dispatch (no macos runner yet)
Some checks failed
build / build-linux (push) Failing after 37s
build / build-macos (push) Has been cancelled
build / release (push) Has been cancelled
2026-08-30 10:19:11 -06:00
trvon
e9c809958b
ci: pin GitHub action references to commit SHAs
Some checks failed
build / build (19, ubuntu-latest) (push) Failing after 48s
build / build (, macos-latest) (push) Has been cancelled
build / release (push) Has been cancelled
2026-08-30 10:04:44 -06:00
trvon
895133942c
ci: use .forgejo/workflows (Forgejo Actions) 2026-08-30 09:41:56 -06:00
trvon
7c82a885eb
ci: port build to Gitea Actions with release packaging 2026-08-30 09:18:45 -06:00
trvon
b10bbd3da6
fix(detect): honor statement-expression write ordering in uninit-read
ARM NEON reductions (vaddvq_*/vminvq_*/vmaxvq_*) expand to GNU statement
expressions `({ ...; __ret = builtin(x); __ret; })`. The uninit-read walker
flat-traversed the statement expression, flagging the result read `__ret`
before noticing the assignment that writes it inside the same expression.
Track variables assigned within a StmtExpr (TraverseStmtExpr) so the result
read is not a false uninit-read.

Verified: simeon neon.cpp range_neon 2->0, statement-expression repro 1->0,
genuine uninit local still fires; full suite 137/137.
2026-08-23 15:47:21 -06:00
trvon
6134bb3c16
fix(detect): recognize power-of-two bitwise guard in div-by-zero
`(d & (d-1)) == 0 ? low & (d-1) : low % d` (simeon sketch_bucket) flagged
`low % d` as division-by-zero, but the guard `(0 & -1) == 0` is TRUE when
d==0, so the `%` arm is unreachable for a zero divisor. Teach
conditionProvesNonZero that `x & (x-1)` clears the lowest set bit (zero iff x
is 0 or a power of two):

  * `(x & (x-1)) == 0` false arm -> x != 0
  * `(x & (x-1)) != 0` true  arm -> x != 0

Also route the same check through analyzeDivCallee so the callsite
precondition for the guarded divisor param is suppressed in the same TU.

Verified: simeon.cpp:48 `proved` finding gone; positive repro 1->0, bare
`a % d` still fires; 59 div-by-zero tests + full suite 137/137.
2026-08-23 15:36:36 -06:00
trvon
ad09feb50f
fix(detect): preserve pointer bindings across imprecise guards + return pairs
Two return-escape FPs:

1. `if (!p) return nullptr;` encodes the null check imprecisely, so walkIf
   took the phi-merge path and replaced `p` with a fresh unknown, losing its
   heap-region binding (yams makeTable/dup_cstr/plugin_host_services_shim).
   Honor then_exits/else_exits even when the condition is imprecise: if one
   arm always exits, the other arm is the only continuation, so skip the merge.

2. `return {v, static_cast<void*>(handle)}` lowers to a CXXConstructExpr (not
   an InitListExpr), and IgnoreParenImpCasts stopped at the explicit cast, so
   `handle` was never marked retained (yams expose_as_c_abi_with_state). Use
   IgnoreCasts and handle the CXXConstructExpr (pair) shape.

Verified: object_storage_adapter.cpp 3->0, plugin_host_services_shim 1->0,
repro 3->0, raw new still fires. Full suite 137/137.
2026-08-23 14:54:43 -06:00
trvon
fb5216c78d
fix(detect): recognize smart-pointer ownership in memory-leak
std::unique_ptr<T>(new T(...)) / std::shared_ptr<T>(new T(...)) hands the
allocation to the smart pointer, whose destructor frees it — not a leak. The
detector flagged these (yams peer_registry.cpp:99, embedded_service_host.cpp:384).

The init is wrapped as ExprWithCleanups -> CXXFunctionalCastExpr ->
CXXBindTemporaryExpr -> CXXConstructExpr -> CXXNewExpr; unwrap those wrappers,
detect the unique_ptr/shared_ptr type, and mark the new-expression's site so
checkLeaks skips it. A raw `new T` with no owner still reports.

Verified: repro 1->0, yams peer_registry + embedded_service_host 1->0 each,
raw new still fires. Full suite 137/137.
2026-08-23 12:54:27 -06:00
trvon
a27c3366d8
fix(symbolic): cache string size()/length() across assignments
std::string/std::string_view size()/length() was cached only in the member
cache, which assign() clears. So `char* p = malloc(s.size()+1); ...; p[s.size()]`
encoded the two size() calls as DIFFERENT symbols, letting the solver find a
spurious out-of-bounds (yams object_storage_adapter.cpp dup_cstr, plugin_host_services_shim).

Route zero-arg const size()/length() through the assignment-surviving
string-length cache (string_lengths_), so both calls resolve to one length
symbol: the final write lands exactly at the end (in-bounds), while a genuine
size()+1 write still fires.

Adds positive/negative oob tests; full suite 137/137.
2026-08-23 09:11:45 -06:00
trvon
8750eb51ea
fix(symbolic): escalate per-query timeout once, not 4x
The 4-minute (8x) escalation ceiling let a single TU with ~27 hard queries
pin a worker for 109 minutes (observed: 1 worker stuck at 99% CPU, scan
stalled 2h). Escalate once (2x base, capped 60s) so "just barely over
budget" queries still resolve, but a TU dense with hard queries can't stall
a worker for hours.
2026-08-22 16:36:14 -06:00
trvon
9c0b5f5e49
fix: bound escalation ceiling and finish score_fp.py lint cleanup
- checkAdaptive: cap escalation at 4 min (8x base) instead of 1 hour. The
  1-hour ceiling let a genuinely-hard query pin a worker for an hour with
  zero file completion, stalling the whole scan (observed: 5 workers at 100%
  CPU, log stopped advancing). 4 min still resolves "just barely over budget"
  queries while bounding pathological ones.
- score_fp.py: guard the last bare .read_text(), drop an unused `elapsed`,
  log the fold --version failure instead of bare pass, and suppress the
  runtime sys.path import (reportMissingImports/E402).
2026-08-22 14:20:38 -06:00
trvon
231af6dbd2
feat(cli): generous compute budgets for day-scale scans
Given the engine can afford days of compute (fuzzing-grade), widen the
solver budgets so hard queries resolve instead of timing out:
- checkAdaptive escalation ceiling raised to 1 hour per query (was 8x base)
- budget_seconds 7200 -> 604800 (7 days)
- per_tu_timeout 900 -> 86400 (24h)

A single hard query getting up to an hour of escalating compute is cheap
relative to a lost vulnerability.
2026-08-22 13:45:47 -06:00
trvon
ba3a8cbf46
fix(bench): harden score_fp.py against unhandled IO/parse errors
Replace bare json.loads/int/float calls with small guarded helpers so a
missing or malformed gold/SARIF/compile-DB file exits with a clear message
instead of a raw traceback, and numeric fields degrade to defaults.
2026-08-22 13:45:47 -06:00
trvon
66a0858613
feat(symbolic): adaptive per-query timeout escalation
Instead of a fixed per-query timeout that loses findings, the security
detectors now escalate: a query returning UNKNOWN at the base budget is
re-checked at 2x/4x/8x the base timeout (Z3's incremental solver preserves
search state, so escalation continues rather than restarts). The engine
self-tunes its per-query effort to gather findings, with the cap derived
(8x base) rather than exposed as another knob.

Wired into null-deref, int-overflow, oob, and div-by-zero non-portfolio
check paths. Full suite 137/137.
2026-08-22 10:06:55 -06:00
trvon
4fc979181c
feat(cli): raise scan timeout defaults for large real-world scans
The 1800s solver budget, 6s per-query timeout, and 300s per-TU watchdog were
too tight for large security scans (e.g. 780-file yams), causing Z3 "unknown"
results and preempted TUs. Raise to 7200s budget, 30s per-query, and 900s
per-TU so slow-but-productive TUs finish and complex queries resolve.
2026-08-21 12:15:35 -06:00
trvon
79992a33d5
fix(detect): null-guard alias deref cast in null-deref shouldQuery
UBSan pinned the chromium file_enumerator_posix.cc SIGSEGV: shouldQuery did
`dyn_cast<DeclRefExpr>(value)` on a null `value` (a no-initializer def like
`int *p;`), binding a null reference (Casting.h:110) and dereferencing it in
the optimized build. Use dyn_cast_or_null; the existing `!dre` guard treats a
valueless def as non-aliasing.
2026-08-20 22:19:04 -06:00
trvon
3484eee073
chore(architecture): track int_overflow_analysis/null_deref as 1100-line monoliths
The null-deref hardening and container-bounded callee guards pushed these two
helper files past the 1000-line ratchet; register them with the existing
tracked-monolith mechanism (matching int_overflow.cpp / oob_access.cpp) and
record the next extraction target.
2026-08-20 22:04:21 -06:00
trvon
e158d3fdba
fix(detect): exclude container-bounded string methods from overflow sinks
std::basic_string / std::basic_string_view methods (substr/append/insert/...)
do size()-pos and pos+count arithmetic that is bounded by the container size
invariant, so an unbounded pos/length at a call site is a logic contract, not
signed-overflow UB. analyzeOverflowCallee now skips these, removing the
chromium file_path.cc substr/append FPs (6 signed findings -> 0) while
preserving the 10 genuine unsigned-wrap findings.

Adds positive tests (substr/append call sites are not flagged) and a clean
negative (free-function arithmetic is still flagged).
2026-08-20 21:54:55 -06:00
trvon
5399fb2387
fix(detect): null-safe stripPointerCasts and defIsNullable probes
stripPointerCasts could call IgnoreParenImpCasts() on a null getSubExpr()
(malformed/dependent cast), and defIsNullable used non-null-checked isa/dyn_cast
on its result — both deref null (getStmtClass(this=0x0)) in the optimized
build. Return nullptr from stripPointerCasts and use isa_and_nonnull /
dyn_cast_or_null at the call sites.
2026-08-20 21:40:02 -06:00
trvon
44d713b48b
fix(detect): harden null-deref decl-name and null-safety guards
- getName() on an operator/ctor/dtor decl asserts (Decl.h:302) and is
  unreliable; use getIdentifier() (null for special names) in curlStrLikeBase
  and isStrlenOfArg.
- shouldQuery: return early when the def-use chains are absent (restores the
  documented "short-circuits on !chains" intent), and null-check the reference
  pointee before chaining ->isPointerType().
- defIsNullable: restrict isNullPointerConstant to literal probes (0/nullptr)
  to avoid EvaluateAsRValue-driven record layout on value-dependent defs.

The debug build (assertions on) now completes on chromium file_enumerator_posix.cc.
2026-08-20 21:25:21 -06:00
trvon
9db9930444
fix(detect): harden clang AST evaluation against record-layout crashes
The chromium 8-TU canary crashed with SIGSEGV on every TU: the security
walkers called clang Expr evaluation (EvaluateAsRValue /
isNullPointerConstant) on arbitrary expressions, forcing ASTRecordLayout,
which recurses/blows the stack or derefs null on complex production types.

1. int-overflow seedMemberConstants: only evaluate plain integer literals
   (`this->data = 42`) instead of any `this->member = <expr>` RHS. This was
   the dominant crash (7/8 chromium TUs unblocked; 17 findings now emit).

2. null-deref shouldQuery: guard `chains == nullptr` (restores the
   documented "short-circuits on !chains" intent) and null-check the
   reference pointee before chaining `->isPointerType()`.

3. null-deref defIsNullable: restrict isNullPointerConstant to literal
   probes (0/nullptr/(void*)0) — lossless for the predicate, and avoids
   EvaluateAsRValue-driven layout on value-dependent def values.

null_deref + int_overflow unit tests pass.
2026-08-20 20:11:10 -06:00
trvon
2d8e059990
fix(detect): narrow memory-leak escape suppression
Three changes restore genuine-leak detection while keeping the
process-lifetime / singleton idiom suppressed (SV-COMP memsafety 0->3 TP,
0 FP; yams singletons stay clean):

1. Return escape: only a pointer-typed return value can hand a heap region
   to the caller. The old fallback called markAllRetained() for ANY
   unresolvable return (including `return 0;`, which ends every main),
   suppressing every allocation in the function. Now non-pointer returns
   leave regions unfreed so they report.

2. Placement new: `new(buffer) T` constructs in the caller's storage and
   allocates no heap memory, but the encoder still creates a region for
   OOB-bounds tracking. Mark that region `placement` so the memory-leak
   detector skips it (yams `g_global_io_context_ptr = new(storage) T()`).

3. Atexit gate: a file-scope global/static store is process-lifetime
   retained only when the TU registers an atexit/__cxa_atexit handler.
   Without one, a global store can still leak (SV-COMP lockfree-*).

Validated on SV-COMP memsafety (3 TP / 0 real FP, 100% precision) and yams
(global_io_context placement-new + config_helpers Meyers singletons: 0 FP).
2026-08-20 19:35:50 -06:00
trvon
79702c0625
fix(frontend): normalize stableFunctionId path for deterministic clone IDs
The subprocess fan-out spells a file's path with a non-canonical form (e.g.
tests/integration/../fixtures/...) while the in-process serial path spells it
canonically. stableFunctionId embedded the raw presumed filename, so the same
function produced different clone finding IDs in parallel vs serial runs and
fold:subprocess_fanout flaked/failed. Apply lexically_normal to the path so the
id is identical regardless of how the compile-DB entry spelled it.
2026-08-16 20:53:32 -06:00
trvon
67cf35fc25
feat(analysis): production-reachability structural closure + plumbing
Add the structural reachability closure over the project call graph with
explicit production roots, plus the supporting ast_visitor, def-use, and
finding-query/sarif plumbing.
2026-08-16 16:58:05 -06:00
trvon
296881cc99
feat(index): extend project index/store and TU store
Grow the persistent project index and TU store to support the incremental
project-graph and production-reachability queries used by whole-project scans.
2026-08-16 16:57:36 -06:00
trvon
51720cfd98
feat(binary): P-Code gadget-effects discovery and pointer-arithmetic handling
Add discoverGadgetEffects and address-keyed space-id offset mapping to the
binary P-Code scan, and recognize PtrAdd/PtrSub/IntAdd/IntSub for binary
pointer-arithmetic analysis.
2026-08-16 16:55:18 -06:00
trvon
bce85eb5f5
feat(cli): scan orchestration, profiling, and reachability plumbing
Update scan router/runner/command, source call-graph contribution, project-graph
orchestrator, production-reachability findings, detector plan/profile
accumulation, fuzz-prep runner, and translation-unit fingerprinting; regenerate
the embedded skill to match the renamed fold-sast skill.
2026-08-16 16:54:57 -06:00
trvon
e4ac03fd5c
feat(detect): recalibrate param-origin findings to latent confidence
Demote parameter-origin detector findings from proved/heuristic to latent
across the source detectors (the callee alone can't prove an extreme value is
reachable), and refine dead-branch prefiltering, uninit-read, sign-conversion,
int-truncation, unused-function, and clone/iterator analysis.
2026-08-16 16:53:43 -06:00
trvon
2a52811aab
feat(symbolic): heap-model array regions and encoder/pcode improvements
Extend the heap model with array-region tracking (allocate/setSymbolicSize/
createHeapArray + zero-offset init) and thread heap-access info through the
expr-arms encoder so member writes past a buffer reach the bounds check;
P-Code encoder improvements for the binary lane.
2026-08-16 16:51:40 -06:00
trvon
fd4a68dfff
test: add coverage for project graph, pcode encoder, and detector behaviors
Unit + integration coverage for the session's work: incremental project
graph/index, pcode encoder, unreachable/dead-branch, equiv/cross-TU,
div-by-zero/sign-conversion/int-truncation, use-after-free, quality-precision,
scan-router profile, source TU runner, detector plan, and the eval fixtures.
2026-08-16 16:46:07 -06:00
trvon
e30764fb19
bench: extend fold_eval scoring with labeled negative cases
Add negative_cases_from_universe to expand an explicit file-by-rule universe
into labeled negatives (enabling TP/FP/TN/FN, not just TP/FP), plus project-eval
harness updates in run_project_eval and the score/score_fp/quality-gate helpers.
2026-08-16 16:43:51 -06:00
trvon
60e2bb7039
ci: pin GitHub Actions to commit SHAs
Pin actions/checkout, setup-python, cache, and upload-artifact to full-length
commit SHAs across the bench, lint, precision-gate, and tsan workflows
(supply-chain hardening; mutable tags can be re-pointed).
2026-08-16 16:42:14 -06:00
trvon
8ac9ddb175
docs: expand fold-sast skill; document call-graph decomposition + budgets
- 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
2026-08-16 16:41:33 -06:00
trvon
53d258563f
fix(build): harden setup.sh meson native-file array + hooks path
Use a bash array for MESON_NATIVE_FILES instead of a space-joined string
(safe word-splitting/quoting), and set core.hooksPath to .githooks so the
tracked lint/pre-push hooks are active after setup.
2026-08-16 16:41:03 -06:00
trvon
14aedf7417
fix(detect): suppress security-detector false positives
- int-overflow: unsigned wrap is defined, not UB — split to
  fold.unsigned-wrap so signed-UB findings aren't drowned; on by
  default (--report-unsigned-wrap=false opts out)
- use-after-free: model return/throw as path-terminating so
  `delete p; return;` in an error branch can't poison the fall-through
- oob-access: real sizeof for complete structs (was 1); recognize
  std::min/clamp/modulo index bounds
- memory-leak: static-storage singletons + return-to-caller are
  caller-owned, not leaks

YAMS scan: UAF 4→0, struct-OOB ~10→0, singleton-leak ~15→0,
min/modulo-bound OOB →0. 137/137 tests pass.
2026-08-16 14:29:35 -06:00
trvon
61ee0b5f4f
fix(build): avoid moving Z3 context 2026-07-30 18:24:29 -04:00
trvon
86b2f14a43
feat(analysis): harden project-wide scans
- persist TU-owned graphs and guarded configuration evidence
- add closed-world production reachability classification
- calibrate detector precision and reviewed self-scan policy
- terminate poisoned workers instead of longjmp recovery
2026-07-30 18:18:04 -04:00
trvon
d0dc830df4
fix(taint): match exec sinks exactly 2026-07-29 23:22:00 -04:00
trvon
2227ff0c69
perf(profile): avoid duplicate fingerprint parse 2026-07-29 23:03:17 -04:00
trvon
0f2e8ca6b6
docs: record quality convergence evidence 2026-07-29 20:47:10 -04:00
trvon
bd29aab697
test(quality): strengthen evaluation evidence 2026-07-29 20:47:04 -04:00
trvon
bcdcb3036b
feat(analysis): harden quality and solver pipelines 2026-07-29 20:46:51 -04:00
trvon
df353e09f8
fix(build): disambiguate Z3 bit-vector value
Linux Z3 overloads cannot resolve an unsigned long long sentinel when uint64_t is unsigned long.
2026-07-29 01:02:20 -04:00
trvon
8d85829481
docs: document convergence contracts 2026-07-29 00:58:19 -04:00
trvon
b0b51f1e71
feat: converge quality and scan interfaces
- fail closed on MCP input and compile database policy
- add shared scan catalogs, finding queries, and tiered gates
2026-07-29 00:57:33 -04:00
trvon
669b4b4b6f
ci: publish quality reports 2026-07-28 14:14:59 -04:00
trvon
ab262b2220
feat(quality): enforce evaluator policies 2026-07-28 14:09:12 -04:00