fold/docs/internal/ghidra-pcode-integration-research.md
trvon 5152d0c9f7
feat(binary): vendor Ghidra 12.1 decompiler + validate P-Code lifting (Phase 0-1)
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>
2026-05-31 17:32:47 -04:00

9.4 KiB

Ghidra Decompiler → SSA P-Code Integration Research (Phase 0)

Generated 2026-05-31. Phase 0 of the "vendor Ghidra decompiler" plan. Docs-first: understand the engine's API, source layout, and build before writing any glue. Sources are the Ghidra repo (NationalSecurityAgency/ghidra) and angr/pypcode (a lift-only vendor we mirror for the build approach).

Decision recap

  • Vendor Ghidra's full decompiler C++ engine (Apache-2.0) at a pinned commit — we control the version, so no ABI skew (the failure mode of the r2ghidra plugin). Consume its SSA high P-Code (Funcdata). Ghidra does data-flow/SSA/variable recovery; fold does Z3 path-reasoning + findings via the existing SafetyObserver::*Ir hooks.
  • Pin: tag Ghidra_12.1_build (Ghidra 12.1, released 2026-05-13). Record the exact 40-char SHA in third_party/ghidra-decompiler/VENDOR.md at clone time.
  • License: Apache-2.0 — vendorable; carry upstream LICENSE + a NOTICE listing the vendored tree, commit SHA, and modifications (build glue only).

1. P-Code semantics (what we consume)

P-Code is a register-transfer IL; every architecture lowers to the same op set, so the encoder is written once. The opcode enum is OpCode in opcodes.hh (CPUI_*). Two levels:

  • Raw P-Code — straight from SLEIGH lifting; per-instruction, no data-flow. (This is all pypcode exposes.)
  • High P-Code (SSA) — after the decompiler's analysis actions run on a Funcdata: SSA form (each Varnode written once), dead code removed, variables/types recovered, def-use explicit. This is what we want — it adds CPUI_MULTIEQUAL (SSA phi), CPUI_INDIRECT, CPUI_PTRADD/CPUI_PTRSUB, CPUI_CAST over the raw set.

Varnode = (address space, offset, size). Spaces we care about: const (immediates → Z3 bv_val), register, unique (temporaries), ram (memory), plus the stack space. A Varnode in const space with offset 0 is the literal zero our current const-hazard pass hunts for — but here it falls out of SSA naturally.

Core ops fold maps to SafetyObserver hooks (the "~25-op core"):

P-Code op fold hook Finding
CPUI_INT_SDIV / INT_UDIV / INT_SREM / INT_REM onDivision / onBinaryArith(Div) div-by-zero (CWE-369) when divisor SAT==0 under path
CPUI_LOAD / CPUI_STORE onPointerDerefIr null-deref (CWE-476) when addr Varnode SAT==0
CPUI_INT_ADD / INT_SUB / INT_MULT onBinaryArith int-overflow (CWE-190); reuse source guard heuristics to avoid every-op FP
CPUI_CALL / CPUI_CALLIND onCallExprIr / onLibraryCall free/malloc/strcpy models → UAF/buffer via existing observers
CPUI_CBRANCH (path predicate) real path-sensitivity → covers div_by_zero_variable / null_deref_conditional
CPUI_COPY / INT_ZEXT / INT_SEXT / SUBPIECE / PIECE value propagation in encoder —
CPUI_MULTIEQUAL (phi) Z3 ite/merge at block joins SSA join
CPUI_INT_EQUAL / INT_SLESS / INT_LESS … branch conditions path constraints

Reference: Ghidra/Features/Decompiler/src/decompile/cpp/opcodes.hh and the rendered P-Code reference Ghidra/Features/Decompiler/src/main/doc/pcoderef.xml.

2. Decompiler C++ embedding API (the glue surface)

Confine all of this to src/binary/ghidra_lifter.cpp (Ghidra types never leak into fold::ir, per the M30 *_clang.cpp convention).

  • LoadImage (loadimage.hh) — abstract byte source. Subclass it to serve a function's bytes from r2 (r2 gives us arch/bits/endian, the function vaddr+size, and raw bytes via p8/prj). Implement loadFill(uint1*, int4, const Address&) + getArchType/adjustVma.
  • SleighArchitecture / Architecture (sleigh_arch.hh, architecture.hh) — owns the translator, spec databases, and the action pipeline. Built from a spec id; specs are loaded via DocumentStorage (xml.hh) reading the .ldefs/.pspec/.cspec and the compiled .sla. Call arch->init(store) to bring up sleigh + types + the default action set.
  • Decompile a function: obtain a Funcdata* for the entry address from the architecture's scope, then run the analysis pipeline: arch->allacts.getCurrent()->reset(*fd); arch->allacts.getCurrent()->perform(*fd); After perform, fd holds high (SSA) P-Code. Iterate ops via fd->beginOpAll() / fd->endOpAll() (each PcodeOp has getOpcode(), getOut(), getIn(i) Varnodes) and basic blocks via fd->getBasicBlocks(). The canonical example is the interactive console consolemain.cc (the decomp_opt binary) and the doxygen "Building a Project" page.
  • Thread-safety: the decompiler uses process/global state (it was designed to run as a one-function-at-a-time child process, ghidra_process). Treat an Architecture as not thread-safe → one instance per fold worker thread, or serialize lifting.

3. Source layout + what to vendor

From Ghidra/Features/Decompiler/src/decompile/cpp/Makefile (authoritative file lists):

  • CORE (lifting/serialization base): xml marshal space float address pcoderaw translate opcodes globalcontext
  • SLEIGH (the lifter): sleigh pcodeparse pcodecompile sleighbase slghsymbol slghpatexpress slghpattern semantics context slaformat compression filemanage
  • DECCORE (the full decompiler / SSA engine — this is the delta over pypcode's lift-only vendor): capability architecture options graph cover block cast typeop database cpool comment stringmanage modelrules fspec action loadimage grammar varnode op type variable varmap jumptable emulate emulateutil flow userop expression multiprecision funcdata funcdata_block funcdata_op funcdata_varnode unionresolve pcodeinject heritage prefersplit rangeutil ruleaction subflow blockaction merge double transform constseq bitfield coreaction condexe override dynamic crc32 prettyprint printlanguage printc printjava memstate opbehavior paramid signature
    • SSA-relevant: heritage (SSA construction), funcdata* (the SSA container), action/ruleaction/coreaction (the simplification rules), merge, flow, blockaction, jumptable.
  • Targets: vendor builds libdecomp.a (= CORE + DECCORE + SLEIGH) and links it into fold. (libsla.a = CORE + SLEIGH only is the lift-only library; not enough for SSA.) sleigh (the spec compiler) is also built to produce .sla.
  • bison/flex generated files: grammar.cc←grammar.y, xml.cc←xml.y, pcodeparse.cc←pcodeparse.y, slghparse.cc/.hh←slghparse.y, slghscan.cc←slghscan.l, ruleparse.cc/.hh←ruleparse.y. → Pre-generate these at vendor time and check the .cc/.hh in, so fold's build needs no bison/flex (matches the "trade runtime-debugging for up-front work" goal).
  • Processor specs: Ghidra/Processors/<arch>/data/languages/*.{slaspec,ldefs,pspec,cspec,sinc}. Compile .slaspec→.sla once with the vendored sleigh binary and check the .sla + .ldefs/.pspec/.cspec in under third_party/ghidra-decompiler/specs/<arch>/. Vendor only the arches we target (below) to keep the tree small.

4. Target architectures (match corpus + bug-bounty reality)

x86 (32) + x86-64, AARCH64 (corpus is arm64 Mach-O), ARM (LE/BE), MIPS (LE/BE). These cover the CTF corpus and the common closed-source targets. More arches are just more checked-in .sla files later — no code change.

5. Build approach (meson)

  • third_party/ghidra-decompiler/ holds the pinned cpp/ sources (with pre-generated parser/lexer .cc/.hh) + specs/<arch>/*.sla + LICENSE + NOTICE + VENDOR.md (commit SHA + the exact upstream paths + the pre-generation/compile commands used).
  • meson: a static-lib target libghidra_decompiler.a over the CORE+DECCORE+SLEIGH .cc set (a files() list mirroring the Makefile vars); C++17, -DLOCAL_ZLIB/bundled zlib as pypcode does, define -DFOLD_HAVE_GHIDRA=1. Gate behind a -Dghidra=auto|enabled|disabled option mirroring the existing radare2 option (meson.build:35), so builds without it fall back to the shipped r2-disasm passes.
  • Smoke: tools/pcode_dump.cpp links the lib, lifts one function from a corpus dylib, prints SSA P-Code ops — validates vendor + spec loading before any fold integration.

6. Reference: pypcode (mirror its build mechanics, not its scope)

angr/pypcode CMakeLists.txt compiles SLEIGH_COMMON (= our CORE+SLEIGH, lift-only: address compression context float globalcontext marshal opcodes pcodecompile pcodeparse pcoderaw semantics slaformat sleigh sleighbase slghpatexpress slghpattern slghsymbol space translate xml), builds the sleigh compiler from + filemanage slgh_compile slghparse slghscan, and runs sleigh -a <processors-dir> at build time to compile .sla. We extend the file set with DECCORE (for SSA) and pre-compile .sla at vendor time instead of at fold build time. pypcode is Apache-2.0/BSD and a good build reference.

7. Open items resolved in later phases

  • Exact pinned SHA for Ghidra_12.1_build → recorded in VENDOR.md at clone (Phase 1).
  • Whether to also expose decompiled C (printc) for a future decompile→Clang→source-detector path → out of scope now; printc is in DECCORE so it's available for free later.
  • Per-function decompile cost vs the binary_backend test budget → measured in Phase 1 with pcode_dump; cap/deadline designed in Phase 4.