[PR #2025] feat(ghidra): admit caller-selected matching dSYM companions #4970

Open
opened 2026-10-31 00:57:32 +00:00 by bsakaguchi · 0 comments
Owner

📋 Pull Request Information

Original PR: https://github.com/morluto/rea/pull/2025
Author: @jimpablo
Created: 10/12/2026
Status: 🔄 Open

Base: main ← Head: feat/dsym-companion-admission


📝 Commits (10+)

  • 4d3b7a1 feat(ghidra): admit caller-selected matching dSYM companions
  • f0eb9c4 fix(targets): partition dSYM identity and match the selected slice
  • 104601e fix(targets): keep legacy query ids and bound companion admission
  • c219cb9 fix(targets): bound uuid walking and carry companion provenance
  • ab3bf42 fix(contracts): budget command spans and partition companions by path
  • 11e1002 fix(ghidra): commit companion identity in the analysis profile
  • 731f243 fix(targets): carry companion identity through split mach-o variants
  • 03f9058 fix(targets): charge traversal reads and bound staged member copies
  • d3fe4a0 fix(ghidra): document and enforce dSYM admission resource budgets
  • dbf8d2c fix(ghidra): preserve dSYM resource refusals and staging coverage

📊 Changes

28 files changed (+2621 additions, -22 deletions)

View changed files

📝 docs/.vitepress/config.ts (+1 -0)
➕ docs/dsym.md (+73 -0)
📝 src/application/BinaryTargetResolver.ts (+80 -10)
➕ src/application/DebugCompanionAdmission.ts (+577 -0)
📝 src/application/DirectAnalysis.ts (+15 -0)
📝 src/application/binary/BinarySessionOpen.ts (+2 -0)
📝 src/application/binary/BinaryTargetSummary.ts (+17 -3)
📝 src/cli/coreBinaryCommands.ts (+2 -0)
📝 src/cli/options.ts (+13 -0)
📝 src/contracts/sessionLifecycleInputs.ts (+19 -0)
📝 src/domain/analysisErrorPresentation.ts (+9 -2)
📝 src/domain/analysisErrorProjection.ts (+2 -1)
📝 src/domain/analysisSnapshot.test.ts (+89 -0)
📝 src/domain/analysisSnapshot.ts (+65 -1)
➕ src/domain/apple/machoLoadCommand.ts (+6 -0)
📝 src/domain/binaryTargetTypes.ts (+19 -0)
📝 src/domain/configurationErrors.ts (+2 -1)
📝 src/domain/liveBinaryTargetSummary.ts (+19 -1)
➕ src/domain/machoDebugCompanion.test.ts (+342 -0)
➕ src/domain/machoDebugCompanion.ts (+228 -0)

...and 8 more files

📄 Description

Summary

Fixes #1193. REA now admits a caller-selected .dSYM bundle for Mach-O executable targets (dsym_path through MCP, --dsym-path on the CLI) and stages it beside the private Ghidra import, so DWARF recovers parameter names, data types, and source-anchored type identity that the stripped executable cannot provide. Admission requires a proven LC_UUID match; a non-matching companion is rejected as caller input instead of silently loading wrong types.

Problem and expected behavior

  • Smallest trigger: an analyst owns a linked Mach-O executable plus its matching .dSYM (built with clang -g + dsymutil). REA's ephemeral import copies the executable into a private runtime root, so the bundle beside the original file is never visible to the session, and every function reports signature_source: "default" with low type confidence.
  • New behavior: with dsym_path, admission (1) validates the bundle shape, (2) requires a DWARF member whose LC_UUID matches the target image's, and (3) stages only the matched member inside REA's runtime root under the snapshot's name — the naming Ghidra's DSymSectionProvider requires — leaving the caller's bundle untouched. The staged member digest, source path, and matched UUID are retained as target-admission evidence. Omitting dsym_path is byte-identical to today.

Change and scope

  • src/domain/machoDebugCompanion.ts (new): pure Mach-O LC_UUID reading for thin and universal containers with bounds checks, and the matching rule (an all-zero UUID is a placeholder that never matches; a target with no usable UUID is rejected explicitly).
  • src/application/DebugCompanionAdmission.ts (new): bundle-shape validation, bounded header reads through the resolver's open target handle, member digesting, and UUID matching.
  • BinaryTargetResolver: optional dsymPath; pairing rejections (dsym with headerless formats, non-Mach-O targets) and admission failures carry a new debug_companion_rejected constraint; the admitted identity rides BinaryTarget.debugCompanion (Mach-O variants only).
  • Contracts: open_binary gains optional absolute dsym_path with contract-level pairing validation; CLI --dsym-path joins nativeOpenOptionFields, so every one-shot native command accepts it; DirectAnalysis, BinarySessionOpen, and the MCP open handler thread it through.
  • GhidraClientOptions.debugCompanion → GhidraClient stages the member beside the snapshot (GhidraDebugCompanion.ts, digest-verified copy, cleanup on failure) and merges dsym facts into the existing target-admission evidence.
  • Error projection: debug_companion_rejected maps to invalid_request/invalid_input with remediation naming the real option spellings, following the load_selection_rejected pattern from #2004.
  • Docs: new docs/dsym.md (+ sidebar entry).
  • Not included: source-file resolution from DWARF, per-slice companion selection, Hopper/IDA staging, and Windows paths (Mach-O only).

Contract and boundary impact

  • Semantic owner and earliest changed stage: companion admission at target resolution (src/domain/machoDebugCompanion.ts + src/application/DebugCompanionAdmission.ts), before any provider starts.
  • CLI and MCP/tool-catalog contract: open_binary input schema gains one optional field; the catalog was regenerated. No output schema changes.
  • Provider, bridge, target-format, or platform compatibility: Ghidra provider stages the companion; other providers leave it unused (documented). No bridge changes — Ghidra's own DWARF analyzer does the ingestion.
  • Evidence, artifact, provenance, or reconstruction contract: target admission evidence gains dsym { source_path, member_sha256, uuid, staged_member }.
  • Process execution, authorization, cleanup, or containment impact: one additional private-runtime copy, removed with the existing runtime-root cleanup.
  • Generated metadata, package, or installation impact: catalog regenerated; docs:check green.

Evidence and regression coverage

  • Tests added: src/domain/machoDebugCompanion.test.ts (10 tests: thin/universal UUID reading, truncation and bounds rejections, zero-UUID and multi-member matching); tests/boundary/filesystem/targetDiagnostics.test.ts gains three — the headerless pairing rejection, and two darwin-gated real-fixture tests that compile two binaries with xcrun clang -g + dsymutil and assert (a) a matching bundle admits with UUID + member digest and (b) the other binary's bundle rejects as caller input with the UUID reason.

  • Base reproduction: with the source changes stashed, all three new boundary tests fail on edf46248f.

  • Real provider verification on this host (macOS 27 arm64, Ghidra 12.1.4 via the Homebrew formula with native mac_arm_64 decompiler, JDK accepted by rea doctor), compiled fixture pair.c → clang -O0 -g -c → link → dsymutil:

    • Before (no companion): signature_source: "default", return-type confidence low.
    • After rea function <pair> _pair_sum --provider ghidra --dsym-path <pair.dSYM> --json: signature int __cdecl pair_sum(Pair p), signature_source: "imported", parameter p of data type Pair (8 bytes, x0:8, confidence high, type-lock evidence), pseudocode local_8 = p.a; uStack_4 = p.b; return local_8 + uStack_4;, value-flow parameter type /DWARF/pair.c/Pair.
    • Rejections through the public CLI: the other binary's bundle → invalid_request/invalid_input, debug_companion_rejected, "contains no DWARF member whose LC_UUID matches"; dsym + --target-format raw-binary → pairing rejection; an unclassifiable blob with dsym keeps its format refusal (ordering preserved).
  • Remaining proof gaps: Linux hosts were not exercised (Mach-O analysis there follows the same path); universal-target companions were not exercised end to end (domain tests cover FAT UUID reading); stdio MCP was exercised through the shared registration, not a separate session run.

  • Pre-existing local failures, reproduced on unmodified edf46248f and unrelated to this change: compactInputSchemaMcp "structurally oversized" pin (upstream 5dfb9d050/#2013 area), enhancedTools.01 call-graph limitations pin, and nibNativeHierarchy on this macOS 27 host.

  • Observed, derived, and inferred claims remain distinguishable.

  • Artifact identity, source provenance, and failed attempts remain preserved.

  • Unsupported, incomplete, unavailable, or uncertain outcomes remain visible.

Validation performed

  • npm run test:focused over machoDebugCompanion.test.ts, targetDiagnostics.test.ts, sessionLifecycleInputs.test.ts, rawBinary.test.ts — all green
  • npx vitest run tests/boundary/cli — 260 passed
  • npx vitest run src/contracts tests/boundary/mcp (after npm run mcp-catalog:generate) — only the two pre-existing upstream failures above
  • npx vitest run src/domain tests/boundary/filesystem tests/composition — only pre-existing failures above
  • npm run check:fast, npm run check (typecheck, lint, format, knip, metadata, architecture guards), npm run docs:check, npm run verify:test-discovery (895 files) — green
  • Real rea function/rea analyze runs with and without the companion as tabled above

Compatibility, safety, and release

  • Breaking changes or migration steps: none. All inputs are optional; omission preserves current behavior.
  • Real Hopper/Ghidra, browser, or OS coverage: real Ghidra 12.1.4 on macOS 27 arm64 as tabled; Hopper/browser not applicable.
  • Package or release metadata impact: regenerated tool catalog only.
  • Security, privacy, process, or containment review: the companion is copied into REA's private runtime root with digest verification and 0600 permissions; the caller's bundle is never modified; no new subprocess or network capability.

Review checklist

  • This PR addresses a concrete problem, not an unscoped refactor.
  • The PR has one focused outcome, and the title follows type(scope): outcome.
  • Related issue is linked.
  • Tests cover changed observable behavior and meaningful failure paths.
  • Owning docs, contracts, and generated metadata are updated.
  • User-visible CLI/MCP changes include representative output (tabled above).
  • I checked the final diff for secrets, unrelated cleanup, and unsupported claims.

Author readiness

Human owner: @jimpablo

  • I reviewed the final diff and relevant existing code, understand the change and its edge cases, and take responsibility for the complete PR, including AI-assisted work.
  • I checked the evidence above, confirmed it supports the current change, and recorded meaningful validation gaps.
  • I declare this PR ready for initial maintainer review.

🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.

## 📋 Pull Request Information **Original PR:** https://github.com/morluto/rea/pull/2025 **Author:** [@jimpablo](https://github.com/jimpablo) **Created:** 10/12/2026 **Status:** 🔄 Open **Base:** `main` ← **Head:** `feat/dsym-companion-admission` --- ### 📝 Commits (10+) - [`4d3b7a1`](https://github.com/morluto/rea/commit/4d3b7a13417a96e90b2a3fd83eede4988a1e157a) feat(ghidra): admit caller-selected matching dSYM companions - [`f0eb9c4`](https://github.com/morluto/rea/commit/f0eb9c484761461148c5f6b54d9be065eceee46f) fix(targets): partition dSYM identity and match the selected slice - [`104601e`](https://github.com/morluto/rea/commit/104601e720356b0553eb608d8f57bbabee8c4946) fix(targets): keep legacy query ids and bound companion admission - [`c219cb9`](https://github.com/morluto/rea/commit/c219cb9d8605e4d2ae8a38cf7fb1191a7a8f7f6d) fix(targets): bound uuid walking and carry companion provenance - [`ab3bf42`](https://github.com/morluto/rea/commit/ab3bf42adfb56e953cfe90d257a38d66a1f2067b) fix(contracts): budget command spans and partition companions by path - [`11e1002`](https://github.com/morluto/rea/commit/11e1002a49a1537bfd8a989e77e28967dec3d6f5) fix(ghidra): commit companion identity in the analysis profile - [`731f243`](https://github.com/morluto/rea/commit/731f2431b5abd5d712fd32858fad08623afae452) fix(targets): carry companion identity through split mach-o variants - [`03f9058`](https://github.com/morluto/rea/commit/03f9058ff86bf28cc74f206b5cee30b8bc0b7e0c) fix(targets): charge traversal reads and bound staged member copies - [`d3fe4a0`](https://github.com/morluto/rea/commit/d3fe4a070f86c052354be0c81f108557a1578752) fix(ghidra): document and enforce dSYM admission resource budgets - [`dbf8d2c`](https://github.com/morluto/rea/commit/dbf8d2c00d6829431496c7c0f272f556d75c1eb0) fix(ghidra): preserve dSYM resource refusals and staging coverage ### 📊 Changes **28 files changed** (+2621 additions, -22 deletions) <details> <summary>View changed files</summary> 📝 `docs/.vitepress/config.ts` (+1 -0) ➕ `docs/dsym.md` (+73 -0) 📝 `src/application/BinaryTargetResolver.ts` (+80 -10) ➕ `src/application/DebugCompanionAdmission.ts` (+577 -0) 📝 `src/application/DirectAnalysis.ts` (+15 -0) 📝 `src/application/binary/BinarySessionOpen.ts` (+2 -0) 📝 `src/application/binary/BinaryTargetSummary.ts` (+17 -3) 📝 `src/cli/coreBinaryCommands.ts` (+2 -0) 📝 `src/cli/options.ts` (+13 -0) 📝 `src/contracts/sessionLifecycleInputs.ts` (+19 -0) 📝 `src/domain/analysisErrorPresentation.ts` (+9 -2) 📝 `src/domain/analysisErrorProjection.ts` (+2 -1) 📝 `src/domain/analysisSnapshot.test.ts` (+89 -0) 📝 `src/domain/analysisSnapshot.ts` (+65 -1) ➕ `src/domain/apple/machoLoadCommand.ts` (+6 -0) 📝 `src/domain/binaryTargetTypes.ts` (+19 -0) 📝 `src/domain/configurationErrors.ts` (+2 -1) 📝 `src/domain/liveBinaryTargetSummary.ts` (+19 -1) ➕ `src/domain/machoDebugCompanion.test.ts` (+342 -0) ➕ `src/domain/machoDebugCompanion.ts` (+228 -0) _...and 8 more files_ </details> ### 📄 Description ## Summary Fixes #1193. REA now admits a caller-selected `.dSYM` bundle for Mach-O executable targets (`dsym_path` through MCP, `--dsym-path` on the CLI) and stages it beside the private Ghidra import, so DWARF recovers parameter names, data types, and source-anchored type identity that the stripped executable cannot provide. Admission requires a proven LC_UUID match; a non-matching companion is rejected as caller input instead of silently loading wrong types. ## Problem and expected behavior - Smallest trigger: an analyst owns a linked Mach-O executable plus its matching `.dSYM` (built with `clang -g` + `dsymutil`). REA's ephemeral import copies the executable into a private runtime root, so the bundle beside the original file is never visible to the session, and every function reports `signature_source: "default"` with low type confidence. - New behavior: with `dsym_path`, admission (1) validates the bundle shape, (2) requires a DWARF member whose `LC_UUID` matches the target image's, and (3) stages only the matched member inside REA's runtime root under the snapshot's name — the naming Ghidra's `DSymSectionProvider` requires — leaving the caller's bundle untouched. The staged member digest, source path, and matched UUID are retained as target-admission evidence. Omitting `dsym_path` is byte-identical to today. ## Change and scope - `src/domain/machoDebugCompanion.ts` (new): pure Mach-O `LC_UUID` reading for thin and universal containers with bounds checks, and the matching rule (an all-zero UUID is a placeholder that never matches; a target with no usable UUID is rejected explicitly). - `src/application/DebugCompanionAdmission.ts` (new): bundle-shape validation, bounded header reads through the resolver's open target handle, member digesting, and UUID matching. - `BinaryTargetResolver`: optional `dsymPath`; pairing rejections (`dsym` with headerless formats, non-Mach-O targets) and admission failures carry a new `debug_companion_rejected` constraint; the admitted identity rides `BinaryTarget.debugCompanion` (Mach-O variants only). - Contracts: `open_binary` gains optional absolute `dsym_path` with contract-level pairing validation; CLI `--dsym-path` joins `nativeOpenOptionFields`, so every one-shot native command accepts it; `DirectAnalysis`, `BinarySessionOpen`, and the MCP open handler thread it through. - `GhidraClientOptions.debugCompanion` → `GhidraClient` stages the member beside the snapshot (`GhidraDebugCompanion.ts`, digest-verified copy, cleanup on failure) and merges `dsym` facts into the existing target-admission evidence. - Error projection: `debug_companion_rejected` maps to `invalid_request`/`invalid_input` with remediation naming the real option spellings, following the `load_selection_rejected` pattern from #2004. - Docs: new `docs/dsym.md` (+ sidebar entry). - Not included: source-file resolution from DWARF, per-slice companion selection, Hopper/IDA staging, and Windows paths (Mach-O only). ## Contract and boundary impact - Semantic owner and earliest changed stage: companion admission at target resolution (`src/domain/machoDebugCompanion.ts` + `src/application/DebugCompanionAdmission.ts`), before any provider starts. - CLI and MCP/tool-catalog contract: `open_binary` input schema gains one optional field; the catalog was regenerated. No output schema changes. - Provider, bridge, target-format, or platform compatibility: Ghidra provider stages the companion; other providers leave it unused (documented). No bridge changes — Ghidra's own DWARF analyzer does the ingestion. - Evidence, artifact, provenance, or reconstruction contract: target admission evidence gains `dsym { source_path, member_sha256, uuid, staged_member }`. - Process execution, authorization, cleanup, or containment impact: one additional private-runtime copy, removed with the existing runtime-root cleanup. - Generated metadata, package, or installation impact: catalog regenerated; `docs:check` green. ## Evidence and regression coverage - Tests added: `src/domain/machoDebugCompanion.test.ts` (10 tests: thin/universal UUID reading, truncation and bounds rejections, zero-UUID and multi-member matching); `tests/boundary/filesystem/targetDiagnostics.test.ts` gains three — the headerless pairing rejection, and two darwin-gated real-fixture tests that compile two binaries with `xcrun clang -g` + `dsymutil` and assert (a) a matching bundle admits with UUID + member digest and (b) the other binary's bundle rejects as caller input with the UUID reason. - Base reproduction: with the source changes stashed, all three new boundary tests fail on `edf46248f`. - Real provider verification on this host (macOS 27 arm64, Ghidra 12.1.4 via the Homebrew formula with native `mac_arm_64` decompiler, JDK accepted by `rea doctor`), compiled fixture `pair.c` → `clang -O0 -g -c` → link → `dsymutil`: - Before (no companion): `signature_source: "default"`, return-type confidence `low`. - After `rea function <pair> _pair_sum --provider ghidra --dsym-path <pair.dSYM> --json`: signature `int __cdecl pair_sum(Pair p)`, `signature_source: "imported"`, parameter `p` of data type `Pair` (8 bytes, `x0:8`, confidence `high`, type-lock evidence), pseudocode `local_8 = p.a; uStack_4 = p.b; return local_8 + uStack_4;`, value-flow parameter type `/DWARF/pair.c/Pair`. - Rejections through the public CLI: the other binary's bundle → `invalid_request`/`invalid_input`, `debug_companion_rejected`, "contains no DWARF member whose LC_UUID matches"; `dsym` + `--target-format raw-binary` → pairing rejection; an unclassifiable blob with `dsym` keeps its format refusal (ordering preserved). - Remaining proof gaps: Linux hosts were not exercised (Mach-O analysis there follows the same path); universal-target companions were not exercised end to end (domain tests cover FAT UUID reading); stdio MCP was exercised through the shared registration, not a separate session run. - Pre-existing local failures, reproduced on unmodified `edf46248f` and unrelated to this change: `compactInputSchemaMcp` "structurally oversized" pin (upstream `5dfb9d050`/`#2013` area), `enhancedTools.01` call-graph limitations pin, and `nibNativeHierarchy` on this macOS 27 host. - [x] Observed, derived, and inferred claims remain distinguishable. - [x] Artifact identity, source provenance, and failed attempts remain preserved. - [x] Unsupported, incomplete, unavailable, or uncertain outcomes remain visible. ## Validation performed - `npm run test:focused` over `machoDebugCompanion.test.ts`, `targetDiagnostics.test.ts`, `sessionLifecycleInputs.test.ts`, `rawBinary.test.ts` — all green - `npx vitest run tests/boundary/cli` — 260 passed - `npx vitest run src/contracts tests/boundary/mcp` (after `npm run mcp-catalog:generate`) — only the two pre-existing upstream failures above - `npx vitest run src/domain tests/boundary/filesystem tests/composition` — only pre-existing failures above - `npm run check:fast`, `npm run check` (typecheck, lint, format, knip, metadata, architecture guards), `npm run docs:check`, `npm run verify:test-discovery` (895 files) — green - Real `rea function`/`rea analyze` runs with and without the companion as tabled above ## Compatibility, safety, and release - Breaking changes or migration steps: none. All inputs are optional; omission preserves current behavior. - Real Hopper/Ghidra, browser, or OS coverage: real Ghidra 12.1.4 on macOS 27 arm64 as tabled; Hopper/browser not applicable. - Package or release metadata impact: regenerated tool catalog only. - Security, privacy, process, or containment review: the companion is copied into REA's private runtime root with digest verification and 0600 permissions; the caller's bundle is never modified; no new subprocess or network capability. ## Review checklist - [x] This PR addresses a concrete problem, not an unscoped refactor. - [x] The PR has one focused outcome, and the title follows `type(scope): outcome`. - [x] Related issue is linked. - [x] Tests cover changed observable behavior and meaningful failure paths. - [x] Owning docs, contracts, and generated metadata are updated. - [x] User-visible CLI/MCP changes include representative output (tabled above). - [x] I checked the final diff for secrets, unrelated cleanup, and unsupported claims. ## Author readiness Human owner: @jimpablo - [x] I reviewed the final diff and relevant existing code, understand the change and its edge cases, and take responsibility for the complete PR, including AI-assisted work. - [x] I checked the evidence above, confirmed it supports the current change, and recorded meaningful validation gaps. - [x] I declare this PR ready for initial maintainer review. --- <sub>🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.</sub>
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
morluto/rea#4970
No description provided.