[GH-ISSUE #5038] [Feature] IDA provider: declare read_bytes with faithful loaded-byte semantics #3094

Open
opened 2026-10-31 00:36:35 +00:00 by bsakaguchi · 0 comments
Owner

Originally created by @Brisbanehuang on GitHub (Oct 18, 2026).
Original GitHub issue: https://github.com/morluto/rea/issues/5038

Before submitting

  • I searched the open and closed issues and pull requests for a related proposal.
  • I understand the contribution scope must be reviewed before integration; open questions are welcome here.
  • This request does not include credentials, private samples, or sensitive target data.

Area

Provider or capability discovery

Current limitation

With the IDA provider, read_bytes is unavailable even though the configured upstream IDA MCP server exposes get_bytes:

$ rea read-bytes <binary> 0x10000080d --length 24 --provider ida --format json
{"error":"Analysis failed","code":"capability_unavailable","category":"unsupported_provider",
 "message":"This analysis is unavailable for the current target. Choose another analysis or target.",
 "details":{"provider_id":"ida","operation":"read_bytes",
            "reason":"operation is not declared by this provider"}}

The same command with --provider ghidra returns the 24 requested bytes.

In rea-agents 6.7.0 the gap is a declaration/dispatch one:

  • dist/ida/IdaProviderCapabilities.js: IDA_OPERATIONS lists 14 operations; read_bytes is absent.
  • dist/ida/IdaSessionClient.js: operationSet = new Set(IDA_OPERATIONS) rejects undeclared operations.
  • dist/ida/IdaOperationRunner.js: run() has no read_bytes branch.

docs/ida-provider.md §Coverage and evidence lists referenced data among the unavailable facets, and docs/mcp-contracts.md documents read_bytes under "Static Ghidra bytes and source mappings", so the current behavior matches the documented scope rather than a regression.

The practical blocker: when a global table is not a recognized string (XOR-obfuscated literal, key schedule, jump table, checksum table), string search can locate the code but the table contents must be read from the provider to ground any conclusion. Today that forces leaving the IDA lane and re-opening the same target in Ghidra for a 24-byte read.

Use cases

  • CTF/crackme-style checks whose comparison table is XOR-obfuscated and therefore absent from the upstream string cache.
  • Verifying constants, relocation targets, or patched bytes at an address produced by IDA analysis.
  • Auditing a Ghidra conclusion against a second engine without switching providers mid-investigation.

Desired outcome and success criteria

  • read_bytes is declared and dispatched for the IDA provider (headless, and attached where the upstream profile supports it).
  • Semantics match the existing contract: returned_bytes and complete stay truthful, and unloaded or unmapped bytes are never reported as observed zeros. A read crossing the first unloaded byte returns the readable prefix with complete: false, as Ghidra does today.
  • The IDA-specific behavior is stated in IDA_LIMITATIONS.
  • Verification: read a file-backed 24-byte range and compare it with the source bytes at vaddr - image_base; read an unmapped address and confirm it is not reported as a successful zero-filled read.

Expected interaction with targets

  • Read existing files or captured data

Target and scope details

  • No target execution. The operation only reads analysis memory from the database IDA already opened; provider startup already launches the headless worker.
  • Arbitrary Python forwarding (py_eval) is outside the documented adapter scope, so a fix should not depend on evaluating IDAPython.
  • Observed on macOS arm64, IDA 9.4 headless via idalib-mcp 2.0.0 (upstream snapshot c133c385), rea-agents 6.7.0.

Verification available so far

I have a prototype or observed results

Verification details

  • CLI reproduction above; --provider ghidra reads the same address successfully on the same artifact.
  • Upstream capability confirmed in the installed BYO server: ida_mcp/api_memory.py::get_bytes(regions: [{addr, size}]) -> [{addr, data}: "0x36 0x11 ..."]. Note the f"{x:#02x}" formatting drops the leading zero for values below 0x10, so a consumer must parse per token rather than strip 0x.
  • Semantic caveat found while drafting a local patch: upstream get_bytes calls read_bytes_bss_safe() (ida_mcp/utils.py), which substitutes 0 wherever ida_bytes.is_loaded() is false and always returns the requested length. Mapping it straight onto read_bytes would claim complete: true for unmapped ranges and fabricate zero bytes, so a faithful implementation needs load status from the upstream side.

Alternatives considered

  • --provider ghidra read_bytes: works, but requires a second engine and a second target open.
  • Mach-O __TEXT mapping (fileoff = vaddr - image_base) plus xxd, the PE/ELF equivalents, or inspect_macho / inspect_artifact: reads the shipped file, not the provider's analysis memory (loader fixups, patched bytes, BSS).
  • Calling the upstream get_bytes tool directly outside REA: works, but produces evidence outside REA's ledger and bypasses its output-schema checks.
  • py_eval: excluded by the documented adapter scope.

Possible direction

  1. Upstream: extend get_bytes with a loaded mask or truncated_at (or add get_bytes_verbose), since only IDA knows is_loaded per byte.
  2. REA: declare read_bytes, map the reply, truncate at the first unloaded byte, set complete = returned_bytes == requested_bytes, push a limitation, and cover it in the IDA adapter tests plus scripts/verify-real-ida.mjs.
  3. If an upstream extension is not acceptable: declare the operation with an explicit limitation that unloaded bytes are zero-filled — but please avoid reporting unmapped reads as successful observations.

References

  • Precedent: #2460 (list_documents undeclared by the IDA adapter) and its resolution.
  • rea-agents 6.7.0: dist/ida/IdaProviderCapabilities.js, dist/ida/IdaSessionClient.js, dist/ida/IdaOperationRunner.js.
  • contracts/toolOutputSchemaGroups.js read_bytes output schema (bytes_hex length, returned_bytes <= requested_bytes, complete equality).
  • docs/ida-provider.md §Coverage and evidence; docs/mcp-contracts.md §Static Ghidra bytes and source mappings.
  • Upstream: mrexodia/ida-pro-mcp ida_mcp/api_memory.py (get_bytes) and ida_mcp/utils.py (read_bytes_bss_safe).
Originally created by @Brisbanehuang on GitHub (Oct 18, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/5038 ### Before submitting - [x] I searched the open and closed issues and pull requests for a related proposal. - [x] I understand the [contribution scope](https://github.com/morluto/rea/blob/main/CONTRIBUTING.md#contribution-scope) must be reviewed before integration; open questions are welcome here. - [x] This request does not include credentials, private samples, or sensitive target data. ### Area Provider or capability discovery ### Current limitation With the IDA provider, `read_bytes` is unavailable even though the configured upstream IDA MCP server exposes `get_bytes`: ``` $ rea read-bytes <binary> 0x10000080d --length 24 --provider ida --format json {"error":"Analysis failed","code":"capability_unavailable","category":"unsupported_provider", "message":"This analysis is unavailable for the current target. Choose another analysis or target.", "details":{"provider_id":"ida","operation":"read_bytes", "reason":"operation is not declared by this provider"}} ``` The same command with `--provider ghidra` returns the 24 requested bytes. In rea-agents 6.7.0 the gap is a declaration/dispatch one: - `dist/ida/IdaProviderCapabilities.js`: `IDA_OPERATIONS` lists 14 operations; `read_bytes` is absent. - `dist/ida/IdaSessionClient.js`: `operationSet = new Set(IDA_OPERATIONS)` rejects undeclared operations. - `dist/ida/IdaOperationRunner.js`: `run()` has no `read_bytes` branch. `docs/ida-provider.md` §Coverage and evidence lists referenced data among the unavailable facets, and `docs/mcp-contracts.md` documents `read_bytes` under "Static Ghidra bytes and source mappings", so the current behavior matches the documented scope rather than a regression. The practical blocker: when a global table is not a recognized string (XOR-obfuscated literal, key schedule, jump table, checksum table), string search can locate the code but the table contents must be read from the provider to ground any conclusion. Today that forces leaving the IDA lane and re-opening the same target in Ghidra for a 24-byte read. ### Use cases - CTF/crackme-style checks whose comparison table is XOR-obfuscated and therefore absent from the upstream string cache. - Verifying constants, relocation targets, or patched bytes at an address produced by IDA analysis. - Auditing a Ghidra conclusion against a second engine without switching providers mid-investigation. ### Desired outcome and success criteria - `read_bytes` is declared and dispatched for the IDA provider (headless, and attached where the upstream profile supports it). - Semantics match the existing contract: `returned_bytes` and `complete` stay truthful, and unloaded or unmapped bytes are never reported as observed zeros. A read crossing the first unloaded byte returns the readable prefix with `complete: false`, as Ghidra does today. - The IDA-specific behavior is stated in `IDA_LIMITATIONS`. - Verification: read a file-backed 24-byte range and compare it with the source bytes at `vaddr - image_base`; read an unmapped address and confirm it is not reported as a successful zero-filled read. ### Expected interaction with targets - Read existing files or captured data ### Target and scope details - No target execution. The operation only reads analysis memory from the database IDA already opened; provider startup already launches the headless worker. - Arbitrary Python forwarding (`py_eval`) is outside the documented adapter scope, so a fix should not depend on evaluating IDAPython. - Observed on macOS arm64, IDA 9.4 headless via idalib-mcp 2.0.0 (upstream snapshot `c133c385`), rea-agents 6.7.0. ### Verification available so far I have a prototype or observed results ### Verification details - CLI reproduction above; `--provider ghidra` reads the same address successfully on the same artifact. - Upstream capability confirmed in the installed BYO server: `ida_mcp/api_memory.py::get_bytes(regions: [{addr, size}]) -> [{addr, data}: "0x36 0x11 ..."]`. Note the `f"{x:#02x}"` formatting drops the leading zero for values below 0x10, so a consumer must parse per token rather than strip `0x`. - Semantic caveat found while drafting a local patch: upstream `get_bytes` calls `read_bytes_bss_safe()` (`ida_mcp/utils.py`), which substitutes 0 wherever `ida_bytes.is_loaded()` is false and always returns the requested length. Mapping it straight onto `read_bytes` would claim `complete: true` for unmapped ranges and fabricate zero bytes, so a faithful implementation needs load status from the upstream side. ### Alternatives considered - `--provider ghidra` `read_bytes`: works, but requires a second engine and a second target open. - Mach-O `__TEXT` mapping (`fileoff = vaddr - image_base`) plus `xxd`, the PE/ELF equivalents, or `inspect_macho` / `inspect_artifact`: reads the shipped file, not the provider's analysis memory (loader fixups, patched bytes, BSS). - Calling the upstream `get_bytes` tool directly outside REA: works, but produces evidence outside REA's ledger and bypasses its output-schema checks. - `py_eval`: excluded by the documented adapter scope. ### Possible direction 1. Upstream: extend `get_bytes` with a loaded mask or `truncated_at` (or add `get_bytes_verbose`), since only IDA knows `is_loaded` per byte. 2. REA: declare `read_bytes`, map the reply, truncate at the first unloaded byte, set `complete = returned_bytes == requested_bytes`, push a limitation, and cover it in the IDA adapter tests plus `scripts/verify-real-ida.mjs`. 3. If an upstream extension is not acceptable: declare the operation with an explicit limitation that unloaded bytes are zero-filled — but please avoid reporting unmapped reads as successful observations. ### References - Precedent: #2460 (`list_documents` undeclared by the IDA adapter) and its resolution. - rea-agents 6.7.0: `dist/ida/IdaProviderCapabilities.js`, `dist/ida/IdaSessionClient.js`, `dist/ida/IdaOperationRunner.js`. - `contracts/toolOutputSchemaGroups.js` `read_bytes` output schema (`bytes_hex` length, `returned_bytes <= requested_bytes`, `complete` equality). - `docs/ida-provider.md` §Coverage and evidence; `docs/mcp-contracts.md` §Static Ghidra bytes and source mappings. - Upstream: mrexodia/ida-pro-mcp `ida_mcp/api_memory.py` (`get_bytes`) and `ida_mcp/utils.py` (`read_bytes_bss_safe`).
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#3094
No description provided.