10 KiB
Library-expansion matrix — outstanding follow-ups
After the sequential sweep documented in
docs/internal/library-expansion-matrix.md, three follow-up workstreams
remain. None block any shipped row; they cover the loose-thread
cleanup the audit pattern surfaced.
Ordering reflects priority. (1) is needed for the C5 detector to be runnable end-to-end; (2) addresses a real capability gap catalogued during the C4 audit; (3) closes the methodology loop on rows that shipped before the per-row baseline convention existed.
1. scan_runner.cpp dispatch hook for uninit-local-read
Status: verified locally on 2026-05-26. The scan-runner diff contains
both C2's selective clang::CallGraph merge and C5's
uninit-local-read dispatch hook. Focused tests and a CLI smoke passed; the
changes are still uncommitted in the worktree.
Background. The C5 wiring commit (48dac13d5) landed:
include/fold/detect/uninit_local_read.hppsrc/detect/uninit_local_read.cppFindingKind::UninitLocalReadininclude/fold/report/finding.hpp- SARIF rule id + name in
src/report/sarif.cpp - finding-name parser in
src/report/finding_json.cpp - registry entry in
src/cli/detector_registry.cpp - 5 unit tests + meson wiring
…but did not commit the four scan_runner.cpp hunks that:
- include
fold/detect/uninit_local_read.hpp - set
want_uninit_local = detectorEnabled(...)and theaddDetectorProfile("uninit-local-read")entry - invoke
detectUninitLocalReadInContext(ctx)inside the per-TU detector dispatch, annotating findings + accumulating profile - emit the
spdlog::info("uninit-local-read: emitted …")line in the summary section
The hunks were withheld because scan_runner.cpp was simultaneously
being edited by a parallel session implementing C2 (clang::CallGraph
selective adapter — mergeClangCallGraph, addLocalCallGraphCallee,
etc.). Both changes are correct individually; the bundled diff just
needed a coordinated commit moment that never arrived.
What to do. Audit the current src/cli/scan_runner.cpp diff —
it should contain BOTH C2 and C5 hunks. Run meson test -C build/release (expected 57/57). Commit as a single bundled commit
crediting both changes, e.g.:
feat(cli): wire C2 clang::CallGraph + C5 uninit-local-read dispatch
- C2: mergeClangCallGraph + addLocalCallGraphCallee augment
fold's existing ir::ProjectCallGraph with clang TU-local edges
- C5: scan-runner dispatch for the uninit-local-read detector
registered in 48dac13d5
Pre-commit clang-format hook will catch two long-line violations
(at the include block + a const_cast line); run clang-format -i src/cli/scan_runner.cpp first.
Verification. After commit, the canonical smoke is:
build/release/src/cli/fold scan \
--compile-db /tmp/c5wiring_smoke/compile_commands.json \
--detectors uninit-local-read \
-o /tmp/out.json -f json
# Expected: "uninit-local-read: emitted 3 findings"
(/tmp/c5wiring_smoke/uninit.c was the fixture used during
C5-wiring development; recreate from the test cases in
tests/unit/test_uninit_local_read.cpp if it's gone.)
2. Native UAF detector
Status: implemented locally on 2026-05-26 as an intra-procedural,
heuristic use-after-free source detector with unit tests and CLI dispatch.
Curl-scale baseline captured at
docs/internal/perf-audit/runs/matrix/UAF-curl-2026-05-26.json (18 SARIF
findings after report filtering; 11 ms detector wall).
Carved out from C4 (docs/internal/library-expansion-matrix.md). C4's
original framing was clang::consumed::ConsumedAnalysis for
RAII / use-after-free. Audit (commit 1dc20ca) confirmed
ConsumedAnalysis only activates on classes marked
[[clang::consumable]] + member functions marked
[[clang::callable_when(state)]] + [[clang::return_typestate(state)]].
Curl/yams/kernel-style C code has none of these attributes.
The capability gap is real: fold has no UAF detector. The right implementation is not ConsumedAnalysis but a fold-native CFG-walker shape:
Sketch.
- New detector:
include/fold/detect/use_after_free.hpp,src/detect/use_after_free.cpp FindingKind::UseAfterFreeininclude/fold/report/finding.hpp- SARIF rule id
fold.use-after-free+ finding-name parser - Registry entry
"use-after-free"insrc/cli/detector_registry.cpp - scan_runner.cpp dispatch (same template as
uninit-local-read)
Detector core (per-function walk):
- Track local set
freed_ptrs: VarDecl* -> first_free_loc - On
free(p),delete p,delete[] p: addp(and any aliases viaq = p;) tofreed_ptrs - On any subsequent read of
p(DeclRefExpr usage, MemberExpr base, deref*p, subscriptp[…]): emit a UAF finding citing both the free site and the use site - Reset on re-assignment (
p = …after the free clears it from the set)
Soundness scope.
- Intra-procedural only (interprocedural via summaries is a follow-up — the existing CrossTUCache pattern fits)
- Trivially correct on flat function bodies; needs CFG awareness
for branches (use the same
BranchLiveness/walkIfscaffolding the security_walker uses) - False-positive sources to handle explicitly:
free(NULL)doesn't poison anything;p = realloc(p, n)is not a UAF even thoughpis conceptually "freed" — modelreallocas a refresh
Test plan.
tests/unit/test_use_after_free.cppwith: simplefree(p); *p;flagged; freed-but-reassigned not flagged;free(NULL)not flagged;p = realloc(p, n)not flagged; alias poisoning;delete p; p->x; and conditional free/use inside the then-branch.- End-to-end SARIF smoke on a synthetic fixture (mirroring the
c5wiring_smokepattern). - Curl baseline finding count after the detector lands —
capture under
docs/internal/perf-audit/runs/matrix/uaf-curl-<DATE>.json.
Out of scope for the first PR.
- C++ smart-pointer typestate (covered by C4's deferred form)
- Interprocedural UAF (free in callee, use in caller) — relies
on a
freed_ptr_paramsslot indetect::CalleeProperties - Heap-region precision via
symbolic::HeapModel(isUseAfterFreealready exists but the connector to the new detector is the C++-aware bit)
Naming. "use-after-free" matches the SARIF taxonomy and CWE-416
("Use After Free"). The existing HeapAccessInfo::is_use_after_free
field already uses this spelling.
3. Per-row precision baselines for shipped rows
Status: Z4, C5, C7, and the native UAF follow-up now have local
baselines under docs/internal/perf-audit/runs/matrix/:
Z4-curl-2026-05-26.jsonC5-curl-2026-05-26.jsonC7-curl-2026-05-26.jsonUAF-curl-2026-05-26.jsonC9-synthetic-2026-05-26.jsonZ5-synthetic-2026-05-26.jsonZ10-serialize-corpus-2026-05-26.jsonC8-contracts-synthetic-2026-05-26.json
All listed shipped-row baseline artifacts now have at least one local artifact. Curl-scale artifacts for Z5/C8/C9 remain optional follow-up work where the existing blockers or cost justify a dedicated session.
What to capture per row.
| Row | Target | Tool | Output |
|---|---|---|---|
| Z5 | synthetic minimal-witness fixture | one-off C++ invoking refineModelForMinimalWitness |
done: docs/internal/perf-audit/runs/matrix/Z5-synthetic-2026-05-26.json; curl fuzz-prep remains blocked by pre-existing crash |
| Z10 | encoder-shaped n=100 corpus | one-off C++ invoking z3 to_string() with alias thresholds |
done: docs/internal/perf-audit/runs/matrix/Z10-serialize-corpus-2026-05-26.json |
| C5 | curl baseline | fold scan --detectors uninit-local-read --profile |
done: docs/internal/perf-audit/runs/matrix/C5-curl-2026-05-26.json |
| C7 | curl baseline | fold scan --detectors deprecated-api --profile |
done: docs/internal/perf-audit/runs/matrix/C7-curl-2026-05-26.json |
| C8 | synthetic twin baseline | fold scan --detectors null-deref --profile with /*@ requires */ annotations vs without |
done: docs/internal/perf-audit/runs/matrix/C8-contracts-synthetic-2026-05-26.json |
| C9 | synthetic + curl | fold scan --fix-output … |
synthetic done: docs/internal/perf-audit/runs/matrix/C9-synthetic-2026-05-26.json; curl patch-count still optional |
Naming convention.
docs/internal/perf-audit/runs/matrix/<row>-<target>-<YYYY-MM-DD>.json,
matching Z4's path.
Effort. Each curl baseline is ≈9 minutes wall (one --runs 1
of perf_audit_run.py). Total ≈54 min for the four curl-shaped
rows + a few one-off Python invocations for Z10/Z5. Best run in a
single dedicated session with the laptop idle.
Update step. Each row's Status column in
docs/internal/library-expansion-matrix.md gets a link added to the new
baseline file, mirroring Z4's:
**shipped** — `docs/internal/perf-audit/runs/matrix/<row>-<target>-<DATE>.json`,
`tests/unit/test_<row>.cpp`
Cross-cutting cleanup
These showed up repeatedly across the matrix sweep and don't fit any single row, but should be cleaned up at some point:
-
Untracked workspace junk.
profile.json,reach.txt,reach_stderr.txt,scan_output.log,taint_trace.log,trace.log,yams_full_scan.log,yams_security_bench.json,scratch/,tools/fold-rust/-,tools/fold-rust/target/are all ingit status -uas untracked. They're scratch artifacts. Either gitignore them (preferred — these are reproducible outputs) or delete them. -
Parallel-session uncommitted edits in
docs/USAGE.md,docs/internal/rust-frontend.md,src/frontend/rust_backend.cpp,src/mcp/tool_registry.cpp,tests/integration/meson.build,tests/integration/test_cross_tu_security.cpp,tests/integration/test_mcp_stdio.cpp,tools/fold-rust/src/main.rs. These belong to a Rust-frontend workstream that's orthogonal to the library-expansion matrix. Audit and commit separately. -
Tests with chained-comparison warnings flagged by clang-tidy throughout the unit-test sweep (e.g.
REQUIRE(stats.queries > 0 || !findings.empty())parsing asv0 <= v1 != v2). Catch2 macros tickle this lint pattern even though the semantics are fine. Either suppress at the file level or wrap conditions in parens.
Suggested next-session opener
# Resume library-expansion follow-ups (see docs/internal/library-expansion-followups.md)
# Order: (1) scan_runner C2+C5 commit, (2) UAF detector, (3) baselines
(1) is mechanical (~10 minutes including verification). (2) is a real new detector (~half a session including tests + curl smoke). (3) is batch capture (~1 hour curl scans).