[PR #4765] [MERGED] fix(ida): refuse universal Mach-O targets in attached profiles #6398

Closed
opened 2026-10-31 01:14:09 +00:00 by bsakaguchi · 0 comments
Owner

📋 Pull Request Information

Original PR: https://github.com/morluto/rea/pull/4765
Author: @kaoru0822-kitauji
Created: 10/17/2026
Status: ✅ Merged
Merged: 10/17/2026
Merged by: @kaoru0822-kitauji

Base: main ← Head: fix/ida-universal-slice-identity


📝 Commits (1)

  • 5877ff7 fix(ida): refuse universal Mach-O targets in attached profiles

📊 Changes

3 files changed (+101 additions, -1 deletions)

View changed files

📝 docs/ida-provider.md (+1 -1)
📝 src/ida/IdaProvider.ts (+15 -0)
📝 tests/composition/idaProvider.test.ts (+85 -0)

📄 Description

Summary

Fixes #3871

Attached IDA (legacy 1.4 and native MCP) admitted universal Mach-O targets, even though the attached identity check cannot tell which slice the GUI database holds. Every slice shares the recorded whole-file SHA-256. So a database of the x86_64 slice passed the check, and its results were attributed to the host-selected arm64 architecture. Attached admission now refuses universal Mach-O targets with recovery guidance. Headless mode is unchanged and still thins the selected slice.

Problem and expected behavior

Trigger: open_binary with provider_id: "ida" in an attached profile on a universal Mach-O. Admission selects the host architecture. The attached identity tuple (IdaSessionClient#observe: recorded SHA-256, path, image base) compares only the whole-file digest, and neither upstream metadata shape carries processor or slice identity. Headless thinning (IdaWorkspace.copyInput) runs only in headless mode. The violated invariant: the architecture attributed to the analysis must be the architecture that was actually analyzed. When that identity cannot be established, it must stay unresolved, and analysis must fail closed.

Expected: in attached profiles, open_binary on a universal Mach-O fails with a typed refusal before any upstream contact. Thin Mach-O and headless universal analysis keep working.

Change and scope

  • IdaProvider.inspectTargetSupport returns unsupported / target_format_unsupported (diagnostics add mode: "attached") for mach-o-universal targets when the configured mode is attached. This covers both the legacy and native protocols. The reason tells the user how to recover: extract the selected slice with lipo -thin and open the thin file in both IDA and REA, or use headless mode.
  • Why admission: the attached identity contract can never bind a universal target. The only facts available are the whole-file digest (all slices share it) and arch-deduplicated availableArchitectures (which also skips unknown CPU types). Refusing at the provider's target-support boundary gives the registry's typed ProviderSelectionError (target_unsupported) and never contacts the GUI. A per-request check in #observe would have to fail every operation after connecting, without adding anything the admission check lacks.
  • This applies to every universal input, including one with a single listed architecture. Duplicate-architecture slices (for example arm64 and arm64e) and unknown CPU types are left out of availableArchitectures, so a single listed architecture does not prove the file has only one slice.
  • Not included: reading slice or processor identity from upstream metadata. Neither the legacy get_metadata shape nor native ida://idb/metadata that REA consumes exposes it, and no real IDA was available to establish such a field.
  • docs/ida-provider.md documents the attached-mode refusal and the recovery paths.
  • Scope: an ordinary fix within existing boundaries, so no maintainer scope decision is needed.

Contract and boundary impact

  • Semantic owner and earliest changed stage: IdaProvider.inspectTargetSupport (provider target admission), before client creation or any upstream call.
  • Contribution scope fit and linked maintainer decision: not applicable. This is a fail-closed fix inside the existing IDA boundary.
  • CLI and MCP/tool-catalog contract: no schema change. open_binary with IDA attached on a universal Mach-O now returns the existing ProviderSelectionError (target_unsupported) with an ida rejection target_format_unsupported, where it used to succeed.
  • Provider, bridge, target-format, or platform compatibility: attached IDA no longer admits mach-o-universal. Thin Mach-O, ELF, PE and headless universal admission are unchanged. When the IDA configuration is invalid, admission keeps its previous target-only result, because the provider already reports itself unavailable.
  • Evidence, artifact, provenance, or reconstruction contract: no Evidence is produced for attached universal targets, so no Evidence subject can carry an architecture different from the one analyzed.
  • Target selection, authority, upstream behavior, and cleanup details: the refusal happens before connect (connects === 0 in the test). IDA analysis stays read-only, and the GUI database is never saved or closed.
  • Generated metadata, package, or installation impact: none.

Expected effects (check known effects; use documentation-only on its own).
Unchecked items do not establish absence; leave uncertainties visible.

  • Read existing files or captured data.
  • Launch or attach to a process.
  • Inject instrumentation or run scripts/code.
  • Change process memory, registers, files, or configuration.
  • Contact network services.
  • Recover protected code or work around protections.
  • Documentation or usability only; no target interaction.
  • Not sure yet; investigation or maintainer guidance needed.

Evidence and regression coverage

  • Tests added or updated: tests/composition/idaProvider.test.ts adds the "IDA universal Mach-O target identity" cases. They write a two-slice (x86_64 + arm64) FAT fixture and open it through the production AnalysisProviderRegistry / SessionProviderRouter / BinarySession with providerId: "ida" and the fake RecordingIdaMcp connection:
    • legacy attached and native attached registrations: expect ProviderSelectionError / target_unsupported with an ida target_format_unsupported rejection, and zero upstream connects.
    • control: a headless registration still opens the same universal file.
  • Base reproduction (executed): with src/ida/IdaProvider.ts restored from origin/main on the rebased branch, both attached cases fail because open returns { ok: true, ... }, meaning the universal target was admitted (2 failed, 5 passed). With the fix, all 7 pass.
  • User-visible CLI/MCP output: the open is refused with the reason "Attached IDA records only the whole-file digest of a universal Mach-O, so REA cannot verify that the open database is its slice. Extract that slice with lipo -thin and open the thin file in both IDA and REA, or use headless mode."
  • Remaining proof gaps: no real IDA (GUI or headless) ran. The claim that attached IDA records the whole-file digest for a universal input comes from the source and the issue's analysis, not from a real-IDA observation. I did not run a full public CLI/MCP workflow for this refusal. The test exercises the production registry and session boundary with a simulated IDA connection.

For evidence-bearing changes:

  • 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:local -- tests/composition/idaProvider.test.ts with base src/ida/IdaProvider.ts: 2 failed, 5 passed (the attached universal cases fail as described above).
  • npm run test:local -- tests/composition/idaProvider.test.ts src/ida/*.test.ts tests/conformance/ida/providerContracts.test.ts src/application/CapabilityInventory.test.ts (after rebase): 21 files, 410 tests passed.
  • Before rebase, the same set plus src/ghidra/GhidraMipsProfile.test.ts src/ghidra/GhidraRiscvProfile.test.ts src/ghidra/GhidraPowerPcProfile.test.ts (which construct IdaProvider): 24 files, 501 tests passed.
  • npm run test:focused -- tests/boundary/providers/ida/{calleeRelationships,containment,retainedConnectionRetry,selectorProvenance,verifierObservations,verifierFeatureTrace,verifierRequirements}.test.ts tests/boundary/mcp/idaTransport.test.ts (before rebase): 8 files, 165 tests passed.
  • npx tsc --noEmit -p .: exit 0 (before rebase).
  • npm run docs:check: passed (before rebase).
  • npx oxfmt / npx oxfmt --check and npx oxlint on the changed files: clean.

Compatibility, safety, and release

  • Breaking changes or migration steps: attached IDA users who opened universal Mach-O inputs now get a refusal. Migration: open a thin slice (lipo -thin <arch>) in both IDA and REA, or switch to headless mode. The old behavior could attribute results to the wrong architecture, so I did not mark this as breaking. A maintainer may judge otherwise.
  • Real Hopper/Ghidra, browser, or OS coverage: none. No real IDA ran. macOS arm64 host only.
  • Package or release metadata impact: none.
  • Security, privacy, process, or containment review: the refusal happens before any upstream connection. No new process, file write or network access.

Author readiness

Human owner:

  • 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.

Before merge

  • This PR addresses a concrete problem or an agreed enhancement.
  • Material scope and authority questions are resolved, and any required maintainer scope decision covers the final diff.
  • The PR has one focused outcome and the title follows type(scope): outcome.
  • Related issue is linked, or the reason for not linking one is stated above.
  • Tests cover changed observable behavior and meaningful failure paths.
  • Owning docs, contracts, and generated metadata are updated where needed.
  • User-visible CLI/MCP changes include representative output.
  • I checked the final diff for secrets, unrelated cleanup, and unsupported claims.

🤖 Generated with Claude Code


🔄 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/4765 **Author:** [@kaoru0822-kitauji](https://github.com/kaoru0822-kitauji) **Created:** 10/17/2026 **Status:** ✅ Merged **Merged:** 10/17/2026 **Merged by:** [@kaoru0822-kitauji](https://github.com/kaoru0822-kitauji) **Base:** `main` ← **Head:** `fix/ida-universal-slice-identity` --- ### 📝 Commits (1) - [`5877ff7`](https://github.com/morluto/rea/commit/5877ff7d4979d46ad55bce9d3f7e01af93b9982b) fix(ida): refuse universal Mach-O targets in attached profiles ### 📊 Changes **3 files changed** (+101 additions, -1 deletions) <details> <summary>View changed files</summary> 📝 `docs/ida-provider.md` (+1 -1) 📝 `src/ida/IdaProvider.ts` (+15 -0) 📝 `tests/composition/idaProvider.test.ts` (+85 -0) </details> ### 📄 Description ## Summary Fixes #3871 Attached IDA (legacy 1.4 and native MCP) admitted universal Mach-O targets, even though the attached identity check cannot tell which slice the GUI database holds. Every slice shares the recorded whole-file SHA-256. So a database of the x86_64 slice passed the check, and its results were attributed to the host-selected arm64 architecture. Attached admission now refuses universal Mach-O targets with recovery guidance. Headless mode is unchanged and still thins the selected slice. ## Problem and expected behavior Trigger: `open_binary` with `provider_id: "ida"` in an attached profile on a universal Mach-O. Admission selects the host architecture. The attached identity tuple (`IdaSessionClient#observe`: recorded SHA-256, path, image base) compares only the whole-file digest, and neither upstream metadata shape carries processor or slice identity. Headless thinning (`IdaWorkspace.copyInput`) runs only in headless mode. The violated invariant: the architecture attributed to the analysis must be the architecture that was actually analyzed. When that identity cannot be established, it must stay unresolved, and analysis must fail closed. Expected: in attached profiles, `open_binary` on a universal Mach-O fails with a typed refusal before any upstream contact. Thin Mach-O and headless universal analysis keep working. ## Change and scope - `IdaProvider.inspectTargetSupport` returns `unsupported` / `target_format_unsupported` (diagnostics add `mode: "attached"`) for `mach-o-universal` targets when the configured mode is `attached`. This covers both the legacy and native protocols. The reason tells the user how to recover: extract the selected slice with `lipo -thin` and open the thin file in both IDA and REA, or use headless mode. - Why admission: the attached identity contract can never bind a universal target. The only facts available are the whole-file digest (all slices share it) and arch-deduplicated `availableArchitectures` (which also skips unknown CPU types). Refusing at the provider's target-support boundary gives the registry's typed `ProviderSelectionError` (`target_unsupported`) and never contacts the GUI. A per-request check in `#observe` would have to fail every operation after connecting, without adding anything the admission check lacks. - This applies to every universal input, including one with a single listed architecture. Duplicate-architecture slices (for example arm64 and arm64e) and unknown CPU types are left out of `availableArchitectures`, so a single listed architecture does not prove the file has only one slice. - Not included: reading slice or processor identity from upstream metadata. Neither the legacy `get_metadata` shape nor native `ida://idb/metadata` that REA consumes exposes it, and no real IDA was available to establish such a field. - `docs/ida-provider.md` documents the attached-mode refusal and the recovery paths. - Scope: an ordinary fix within existing boundaries, so no maintainer scope decision is needed. ## Contract and boundary impact - Semantic owner and earliest changed stage: `IdaProvider.inspectTargetSupport` (provider target admission), before client creation or any upstream call. - Contribution scope fit and linked maintainer decision: not applicable. This is a fail-closed fix inside the existing IDA boundary. - CLI and MCP/tool-catalog contract: no schema change. `open_binary` with IDA attached on a universal Mach-O now returns the existing `ProviderSelectionError` (`target_unsupported`) with an `ida` rejection `target_format_unsupported`, where it used to succeed. - Provider, bridge, target-format, or platform compatibility: attached IDA no longer admits `mach-o-universal`. Thin Mach-O, ELF, PE and headless universal admission are unchanged. When the IDA configuration is invalid, admission keeps its previous target-only result, because the provider already reports itself unavailable. - Evidence, artifact, provenance, or reconstruction contract: no Evidence is produced for attached universal targets, so no Evidence subject can carry an architecture different from the one analyzed. - Target selection, authority, upstream behavior, and cleanup details: the refusal happens before connect (`connects === 0` in the test). IDA analysis stays read-only, and the GUI database is never saved or closed. - Generated metadata, package, or installation impact: none. Expected effects (check known effects; use documentation-only on its own). Unchecked items do not establish absence; leave uncertainties visible. - [x] Read existing files or captured data. - [ ] Launch or attach to a process. - [ ] Inject instrumentation or run scripts/code. - [ ] Change process memory, registers, files, or configuration. - [ ] Contact network services. - [ ] Recover protected code or work around protections. - [ ] Documentation or usability only; no target interaction. - [ ] Not sure yet; investigation or maintainer guidance needed. ## Evidence and regression coverage - Tests added or updated: `tests/composition/idaProvider.test.ts` adds the "IDA universal Mach-O target identity" cases. They write a two-slice (x86_64 + arm64) FAT fixture and open it through the production `AnalysisProviderRegistry` / `SessionProviderRouter` / `BinarySession` with `providerId: "ida"` and the fake `RecordingIdaMcp` connection: - legacy attached and native attached registrations: expect `ProviderSelectionError` / `target_unsupported` with an `ida` `target_format_unsupported` rejection, and zero upstream connects. - control: a headless registration still opens the same universal file. - Base reproduction (executed): with `src/ida/IdaProvider.ts` restored from `origin/main` on the rebased branch, both attached cases fail because `open` returns `{ ok: true, ... }`, meaning the universal target was admitted (2 failed, 5 passed). With the fix, all 7 pass. - User-visible CLI/MCP output: the open is refused with the reason "Attached IDA records only the whole-file digest of a universal Mach-O, so REA cannot verify that the open database is its <arch> slice. Extract that slice with lipo -thin and open the thin file in both IDA and REA, or use headless mode." - Remaining proof gaps: no real IDA (GUI or headless) ran. The claim that attached IDA records the whole-file digest for a universal input comes from the source and the issue's analysis, not from a real-IDA observation. I did not run a full public CLI/MCP workflow for this refusal. The test exercises the production registry and session boundary with a simulated IDA connection. For evidence-bearing changes: - [ ] 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:local -- tests/composition/idaProvider.test.ts` with base `src/ida/IdaProvider.ts`: 2 failed, 5 passed (the attached universal cases fail as described above). - `npm run test:local -- tests/composition/idaProvider.test.ts src/ida/*.test.ts tests/conformance/ida/providerContracts.test.ts src/application/CapabilityInventory.test.ts` (after rebase): 21 files, 410 tests passed. - Before rebase, the same set plus `src/ghidra/GhidraMipsProfile.test.ts src/ghidra/GhidraRiscvProfile.test.ts src/ghidra/GhidraPowerPcProfile.test.ts` (which construct `IdaProvider`): 24 files, 501 tests passed. - `npm run test:focused -- tests/boundary/providers/ida/{calleeRelationships,containment,retainedConnectionRetry,selectorProvenance,verifierObservations,verifierFeatureTrace,verifierRequirements}.test.ts tests/boundary/mcp/idaTransport.test.ts` (before rebase): 8 files, 165 tests passed. - `npx tsc --noEmit -p .`: exit 0 (before rebase). - `npm run docs:check`: passed (before rebase). - `npx oxfmt` / `npx oxfmt --check` and `npx oxlint` on the changed files: clean. ## Compatibility, safety, and release - Breaking changes or migration steps: attached IDA users who opened universal Mach-O inputs now get a refusal. Migration: open a thin slice (`lipo -thin <arch>`) in both IDA and REA, or switch to headless mode. The old behavior could attribute results to the wrong architecture, so I did not mark this as breaking. A maintainer may judge otherwise. - Real Hopper/Ghidra, browser, or OS coverage: none. No real IDA ran. macOS arm64 host only. - Package or release metadata impact: none. - Security, privacy, process, or containment review: the refusal happens before any upstream connection. No new process, file write or network access. ## Author readiness Human owner: - [ ] 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. ## Before merge - [ ] This PR addresses a concrete problem or an agreed enhancement. - [ ] Material scope and authority questions are resolved, and any required maintainer scope decision covers the final diff. - [ ] The PR has one focused outcome and the title follows `type(scope): outcome`. - [ ] Related issue is linked, or the reason for not linking one is stated above. - [ ] Tests cover changed observable behavior and meaningful failure paths. - [ ] Owning docs, contracts, and generated metadata are updated where needed. - [ ] User-visible CLI/MCP changes include representative output. - [ ] I checked the final diff for secrets, unrelated cleanup, and unsupported claims. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- <sub>🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.</sub>
bsakaguchi 2026-10-31 01:14:09 +00:00
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#6398
No description provided.