Turn a detected overflow + gadget inventory into a candidate ROP chain
scaffold (auto target) attached to the finding.
- rop_chain (pure, fold::binary, unit-tested): synthesizeChain auto-selects a
target — a recovered "win" function (system/execve/puts/printf), else an
execve("/bin/sh") syscall chain assembled from syscall + register-control
gadgets (x0/x1/x2/x8 on AArch64, rdi/rsi/rdx/rax on x86-64). Emits ordered
pad/gadget/value/target steps + explicit caveats (PIE/ASLR base, canary, and
that padding_len is the buffer capacity — the exact saved-return-address
offset is target-frame-dependent and must be verified).
- ghidra_lifter: expose each pointer arg's recovered stack-frame offset
(in_buffer_offsets) alongside its size; signed-offset helper. (Exact RA-slot
recovery is left for future work — the saved return address for these
overflows lives in the caller's frame, which a single-function lift can't see;
honest caveats cover this rather than emit a wrong padding.)
- r2_backend: findStringAddress ("/bin/sh" via r2 /j) to seed execve chains.
- report: ExploitContext gains an optional RopChainInfo (padding_len, target,
steps, caveats); serialized in JSON (write + read round-trip) and SARIF.
- binary_pcode_scan: attachExploitPrimitives now also synthesizes a chain per
buffer-overflow finding from the gadget inventory + recovered capacity.
Validated: baby_bof's decode_base64 overflow carries a chain (target win:printf,
padding 256, steps + caveats). Unit: test_rop_chain (17 assertions). Corpus BOF
counts unchanged; meson test 77/77.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
Drives the dual-stream router over a real binary end-to-end.
- r2_backend: detectBinaryArch() (iIj) + extractFunctionBytes() (aflj + p8,
skipping import thunks) — r2 supplies arch + per-function bytes; the Ghidra
lifter consumes them.
- binary_pcode_scan.cpp (fold-ghidra lib): scanBinaryWithPcode() maps the
r2 arch to a vendored SLEIGH spec (arm64-LE -> AARCH64:LE:64:v8A today;
unknown arch -> {} so the caller falls back to the r2-disasm passes),
enumerates functions, routes each through both lift streams, and merges +
dedups across functions. max_functions caps the sweep (robustness).
- meson: -DFOLD_GHIDRA_SPECS_DIR baked to the vendored specs path so the lifter
locates .ldefs/.sla at runtime; binary_pcode_scan added to fold-ghidra; the
pcode_scan driver gains a whole-binary `pcode_scan <binary>` mode.
Validated on the CTF corpus: libdiv_by_zero.dylib -> exactly 2 div-by-zero
(div_by_zero_direct + mod_by_zero, correctly attributed; div_by_zero_variable
abstains pending path-forking), and 0 on the other 9 dylibs. This reaches
parity with the shipped slice-4 div-by-zero TPs through the proper Z3/P-Code
pipeline. Next (Phase 4 part 2): wire into scan_runner additively (dedup the
div-by-zero overlap with the structural passes), then the encoder increments
(path-forking, null-deref, overflow) for the remaining corpus gains.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
Phase 2 of the Ghidra P-Code binary frontend. Adds the frontend-agnostic
P-Code IR and a dual-mode lifter, validated end-to-end.
- include/fold/ir/pcode.hpp: the ~25-op core P-Code IR (Op/Space/Varnode/
PcodeOp/Function), no Ghidra/clang types (M30 invariant). Function carries a
Source tag (Raw|SSA) so findings record provenance.
- include/fold/binary/ghidra_lifter.hpp + src/binary/ghidra_lifter.cpp
(Ghidra types confined): a buffer LoadImage (no BFD) + two entry points
producing the same ir::pcode::Function:
* liftRaw — Sleigh::oneInstruction, per-instruction RAW P-Code;
* liftSSA — SleighArchitecture + Funcdata + action pipeline, SSA high
P-Code (needs CapabilityPoint::initializeAll + whole-archive link).
Both serialized under one mutex (decompiler global state).
- meson: libghidra_decompiler gains the arch/loader infra (sleigh_arch,
inject_sleigh, loadimage_xml, xml_arch, raw_arch); dep switched to
link_whole so capability self-registration constructors run; pcode_lift
smoke driver added (built at fold's std, links the c++11 lib).
Empirical validation on arm64 sym._div_by_zero_direct confirms the dual-path
rationale: RAW preserves `(w9)=COPY const 0` + `INT_SDIV(w8,w9)` (the hazard),
while SSA folds it to `COPY const 0` (ARM64 sdiv-by-0 returns 0; Ghidra's
optimizer constant-folds x/0 -> 0). The two levels are complementary streams
into the Z3 core (next phases): raw for div/overflow/null-deref, SSA for
UAF/dataflow.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
- symbolic_summary.rs: MIR-based summary computation per function
- Produces SymbolicSummaryData with SMT-LIB BV-64 expressions
- Matches C++ fold::symbolic::SymbolicSummary wire format
- Cross-function Z3 proofs now possible
- MIR-to-IR tree converter: 12 functions walk MIR basic blocks
- Each block becomes Call(mir_block) with statements + terminator
- SwitchInt terminators → path predicate forking per case
- Return/Rvalue encoding threaded through RustExprNode JSON
- Brings Rust CFG precision to parity with Clang security walker
- Swift-mangled symbols (//_T0) skipped: need valid objects, not raw uint64_t
- Cov:2 dead end explained: garbage args cause Swift runtime abort before real code
- Fuzz budget floor raised 30→120s, ceiling 120→600s (match AFL++/libFuzzer norms)
- Base per-CPU allocation doubled 45→90s
- HW detect: sysconf CPU/memory → compute per-harness budget (30-120s)
- --per-harness-timeout-seconds flag for override
- 3-5 seed files per harness: zeros, sentinels, ASCII patterns
- --corpus-seeds N flag to control seed count
- Store harness_file in binary fuzz predictions to fix prep↔run mismatch
- Resolve relative -I flags against compile DB directory
- Add caller_unsafe/is_param_sourced fields to fold-rust NDJSON
- Allow empty target_sources in fuzz-run for dlopen harnesses
- Recursive IR encoder descent for nested hazards
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>